foam-otel 3.0.0.alpha.3 → 3.0.0.alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +55 -17
- data/lib/foam/otel/internal/aws_lambda.rb +69 -0
- data/lib/foam/otel/internal/clients/net_http.rb +16 -7
- data/lib/foam/otel/internal/clients/read_tee.rb +28 -0
- data/lib/foam/otel/internal/constants.rb +12 -3
- data/lib/foam/otel/internal/fork_hooks.rb +23 -0
- data/lib/foam/otel/internal/init.rb +123 -62
- data/lib/foam/otel/internal/instrumentations.rb +3 -3
- data/lib/foam/otel/internal/lifecycle.rb +12 -4
- data/lib/foam/otel/internal/otlp.rb +43 -6
- data/lib/foam/otel/internal/rails_middleware.rb +58 -0
- data/lib/foam/otel/internal/redactor.rb +16 -11
- data/lib/foam/otel/internal/resource.rb +29 -6
- data/lib/foam/otel/internal/slots.rb +52 -0
- data/lib/foam/otel/internal/state.rb +60 -45
- data/lib/foam/otel/internal/traces.rb +10 -2
- data/lib/foam/otel/version.rb +1 -1
- data/lib/foam/otel.rb +7 -2
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34ec502fdc38ac38099bba65f1fc37c91f1edb79476bff598c6a0dda7245fcf0
|
|
4
|
+
data.tar.gz: 9596d8c1d746b66103b87587f5c52006e024d0498d93816912ed916014e563e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85207e2233f8a986bb874f68215960f20216e010978e5d1355808ec2ed6bfb44f19c9b8f8947cc3164cca65864326dab8b5ef95fb8ef6c37a1150fe06923ae2a
|
|
7
|
+
data.tar.gz: 3a3bc463b06b32e1d1bd4b6c439e5f8a92b031859301f8681e78a32e4e1db655ef73d4439a0755e336abc9fc1b068bca48049172046732da4adb514eb12c00cb
|
data/README.md
CHANGED
|
@@ -29,7 +29,20 @@ Store the ingest token as `FOAM_OTEL_TOKEN` and pass `ENV["FOAM_OTEL_TOKEN"]` to
|
|
|
29
29
|
|
|
30
30
|
### Rails
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Create `config/initializers/foam.rb`:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
Foam.init(
|
|
36
|
+
name: "checkout-api",
|
|
37
|
+
environment: Rails.env,
|
|
38
|
+
enabled: true,
|
|
39
|
+
token: ENV["FOAM_OTEL_TOKEN"]
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
During boot, `init` adds Foam's Rack middleware to the app directly after the OpenTelemetry Rack middleware, so server spans get the same header and body capture as under plain Rack. `init` must run before the app initializes: an initializer file or `config/application.rb`. An `init` from `after_initialize` or later warns on stderr and adds no middleware.
|
|
44
|
+
|
|
45
|
+
Nothing else changes; `config.ru` stays as generated. A `RUBYOPT` preload runs before Rails loads, so it installs no Rails instrumentation and adds no middleware.
|
|
33
46
|
|
|
34
47
|
### Plain Ruby and Rack
|
|
35
48
|
|
|
@@ -89,6 +102,8 @@ Every method lives on the `Foam` module. Other than the ingest factories, none o
|
|
|
89
102
|
|
|
90
103
|
Sets up tracing, metrics, logs, automatic instrumentation, W3C `tracecontext` and `baggage` propagation, and OTLP export to Foam.
|
|
91
104
|
|
|
105
|
+
`init` claims the four OpenTelemetry global slots only when they are free: the tracer provider, meter provider, logger provider, and propagator. A slot another SDK set before `init` stays with that SDK. Foam warns once on stderr, reports the signal in `state`, and builds no pipeline for it, so any `additional_*` components for that signal are unused. Foam always builds its own logger provider, so `log` delivers either way. `shutdown` releases the slots Foam claimed.
|
|
106
|
+
|
|
92
107
|
Call this once. Use it when Foam should run OpenTelemetry for the process.
|
|
93
108
|
|
|
94
109
|
```ruby
|
|
@@ -133,7 +148,7 @@ Foam.init(
|
|
|
133
148
|
- `version` - the service version, sent as `service.version` when given.
|
|
134
149
|
- `sample_rate` - the fraction of root traces kept, from `0.0` to `1.0`; child spans follow their parent. Leave it at `1.0` unless volume forces sampling. `OTEL_TRACES_SAMPLER` is not consulted.
|
|
135
150
|
- `additional_span_processors`, `additional_log_record_processors`, `additional_metric_readers` - the app's own OpenTelemetry components, added to Foam's providers after Foam's own exporters.
|
|
136
|
-
- `additional_resource_attributes` - attributes merged into the resource of every signal. `service.name`, `service.version`, and `deployment.environment.name` come from the options above; a value for one of them here is reported on stderr and ignored.
|
|
151
|
+
- `additional_resource_attributes` - attributes merged into the resource of every signal. `service.name`, `service.version`, and `deployment.environment.name` come from the options above and `service.instance.id` from Foam; a value for one of them here is reported on stderr and ignored.
|
|
137
152
|
- `additional_instrumentations` - instrumentations to install next to the bundle. An entry is an instrumentation's registry name, which is its class name without the trailing `::Instrumentation` (`"OpenTelemetry::Instrumentation::Logger"`), or a `[name, config]` pair to install it with options (`["OpenTelemetry::Instrumentation::PG", {db_statement: :include}]`). Require the instrumentation's gem before `init`; an entry whose instrumentation is not loaded fails `init`. An entry naming a bundled instrumentation replaces Foam's configuration for it.
|
|
138
153
|
- `ignored_outbound_hosts` - skips tracing of outbound HTTP requests to these hostnames. The endpoint host is always skipped. Use it for hosts whose traffic would drown the traces, such as a sidecar health check on `localhost` or another vendor's OTLP ingest host. Hosts are compared as written; `localhost` and `127.0.0.1` are different hosts. A request to a skipped host still carries a `traceparent` marked not sampled, so an OpenTelemetry service behind it records nothing for that request.
|
|
139
154
|
- `disable_log_sending` - `true` stops shipping the app's `Logger` lines to Foam. `Foam.log` is unaffected. See Loggers.
|
|
@@ -143,11 +158,19 @@ Foam.init(
|
|
|
143
158
|
|
|
144
159
|
TODO(jhartquist): `propagation_targets`, an allowlist of the hosts that receive `traceparent`, `tracestate`, and `baggage`. Spans stay on for every other host; only the headers are withheld. Today `ignored_outbound_hosts` is the only lever and it drops the spans too. The client patches see the host and can strip the headers after the instrumentation injects them. The endpoint host stays fully ignored.
|
|
145
160
|
|
|
146
|
-
Every signal carries a resource with
|
|
161
|
+
Every signal carries a resource with these attributes:
|
|
162
|
+
|
|
163
|
+
- The service identity from the options above: `service.name`, `deployment.environment.name`, and `service.version` when given.
|
|
164
|
+
- `service.instance.id`, a UUID made for the process at `init`.
|
|
165
|
+
- `telemetry.distro.name` and `telemetry.distro.version`, naming this gem.
|
|
166
|
+
- `host.name`, `host.arch`, `host.id` when it can be read, `os.type`, and `os.version`.
|
|
167
|
+
- The process and SDK attributes the OpenTelemetry SDK adds.
|
|
168
|
+
|
|
169
|
+
A process forked after `init` carries its own `service.instance.id` and `process.pid`. `OTEL_RESOURCE_ATTRIBUTES` can add further attributes. It cannot override any of the attributes above, and `OTEL_SERVICE_NAME` cannot override `name`.
|
|
147
170
|
|
|
148
171
|
`init` never raises. A failure or an option holding an invalid value is reported on stderr with a `[foam-otel]` prefix and leaves `state` uninitialized.
|
|
149
172
|
|
|
150
|
-
`init` also reports its outcome to Foam: one log record with the message and the `state`, sent straight to the endpoint with the token.
|
|
173
|
+
`init` also reports its outcome to Foam: one log record with the message and the `state`, sent straight to the endpoint with the token. The report is sent in the background and never blocks the app. When the process exits before the report has been delivered, exit waits up to two seconds for the send to finish; a report that cannot be delivered is dropped. Nothing is sent without a token.
|
|
151
174
|
|
|
152
175
|
### `set_endpoint(url)`
|
|
153
176
|
|
|
@@ -229,7 +252,7 @@ OpenTelemetry::Context.with_current(context) do
|
|
|
229
252
|
end
|
|
230
253
|
```
|
|
231
254
|
|
|
232
|
-
|
|
255
|
+
`inject_trace_context` and `extract_trace_context` always use the W3C `traceparent`, `tracestate`, and `baggage` headers, whatever propagator holds the global slot. Foam does not read `OTEL_PROPAGATORS`. An app that needs a different propagator registers it before `init`, and Foam leaves it in place.
|
|
233
256
|
|
|
234
257
|
### `set_baggage(key, value)`
|
|
235
258
|
|
|
@@ -287,9 +310,9 @@ rescue PaymentError => e
|
|
|
287
310
|
end
|
|
288
311
|
```
|
|
289
312
|
|
|
290
|
-
The span gets an `exception` event with the type, message, and stack trace, plus any `attributes`, and its status becomes error with the exception's message. The active span is used whichever SDK started it. Without an active span nothing is recorded.
|
|
313
|
+
The span gets an `exception` event with the type, message, and stack trace, plus any `attributes`, and its status becomes error with the exception's message. A value that is not an `Exception` is recorded by its string form as the message, with no type or stack trace. The active span is used whichever SDK started it. Without an active span nothing is recorded.
|
|
291
314
|
|
|
292
|
-
### `flush(timeout:
|
|
315
|
+
### `flush(timeout: 10)`
|
|
293
316
|
|
|
294
317
|
Exports everything Foam's providers still hold. Call it before a point where the process may be killed without a normal exit, for example at the end of a job on a runner that stops the process.
|
|
295
318
|
|
|
@@ -299,14 +322,16 @@ Foam.flush(timeout: 5)
|
|
|
299
322
|
|
|
300
323
|
`timeout` is in seconds and bounds the whole call. A provider that runs out of time is reported on stderr.
|
|
301
324
|
|
|
302
|
-
### `shutdown(timeout:
|
|
325
|
+
### `shutdown(timeout: 10)`
|
|
303
326
|
|
|
304
|
-
Flushes and shuts down Foam's providers, including the one behind `log` when an ingest component created it. Both `init` and the ingest log factory register it with `at_exit`, so most apps never call it.
|
|
327
|
+
Flushes and shuts down Foam's providers, including the one behind `log` when an ingest component created it, and releases the global slots Foam claimed. Both `init` and the ingest log factory register it with `at_exit`, so most apps never call it. An app calls it before a process is killed without a normal exit.
|
|
305
328
|
|
|
306
329
|
`timeout` is in seconds and bounds the whole call. A provider that runs out of time is reported on stderr.
|
|
307
330
|
|
|
308
331
|
`at_exit` covers a normal exit, an unhandled exception, and a signal the process handles by exiting. `exit!`, `KILL`, and a crash lose the last batch, and so does a batch that outlasts the timeout. An exception outside any span is invisible unless the app logs it.
|
|
309
332
|
|
|
333
|
+
Foam's span processor holds 4096 finished spans and drops the oldest when full; `OTEL_BSP_MAX_QUEUE_SIZE` overrides the size. The ingest span processor and the log record processors keep the OpenTelemetry default of 2048.
|
|
334
|
+
|
|
310
335
|
### `state`
|
|
311
336
|
|
|
312
337
|
Returns whether Foam initialized, which instrumentations registered, which signals have an export path, and the identity `init` was given.
|
|
@@ -342,7 +367,7 @@ Each `signals` value names the source of the Foam export path for that signal:
|
|
|
342
367
|
- `:local` - logs only: another SDK owns the global provider and Foam keeps its own so `log` still delivers.
|
|
343
368
|
- `:none` - no Foam export path.
|
|
344
369
|
|
|
345
|
-
`:global` is checked against the live OpenTelemetry global
|
|
370
|
+
A slot another SDK held before `init` stays with it and reports `:none` (`:local` for logs) from the start. Foam builds no provider for that signal, so the metric helpers and `set_baggage` do nothing while `log` still delivers. `:global` is checked against the live OpenTelemetry global on each call, so a slot another SDK takes after `init` reports the same way; in that case only the slot changes hands, and the helpers keep using Foam's own providers. Both cases are warned about once on stderr. Body capture is off while another SDK holds the logs slot. `baggage` is `:global` or `:none`. `profile` is not supported and always reports `:none`.
|
|
346
371
|
|
|
347
372
|
## Custom spans and other OpenTelemetry APIs
|
|
348
373
|
|
|
@@ -360,7 +385,7 @@ There is no option to record other headers.
|
|
|
360
385
|
|
|
361
386
|
### Header capture
|
|
362
387
|
|
|
363
|
-
Server spans get the headers from `Foam::Rack::Middleware`, placed after the OpenTelemetry Rack middleware
|
|
388
|
+
Server spans get the headers from `Foam::Rack::Middleware`, placed after the OpenTelemetry Rack middleware (added by `init` in Rails apps).
|
|
364
389
|
|
|
365
390
|
Client spans get the headers from Foam's hooks on `Net::HTTP`, Excon, HTTPClient, and Faraday, applied unless `network_capture` is `:off`. The `Net::HTTP` hook is always installed; the others install only when their gem is loaded before `init`, and Faraday's covers connections built after `init`. Faraday is covered on the Faraday span whatever the adapter. `http`, `httpx`, `ethon`, and `restclient` are not covered.
|
|
366
391
|
|
|
@@ -380,7 +405,7 @@ In `:advanced` mode each captured body arrives as `foam.http.body.chunk` log rec
|
|
|
380
405
|
|
|
381
406
|
### Rack bodies
|
|
382
407
|
|
|
383
|
-
Server bodies need `Foam::Rack::Middleware` after the OpenTelemetry Rack middleware, as shown under "Plain Ruby and Rack"
|
|
408
|
+
Server bodies need `Foam::Rack::Middleware` after the OpenTelemetry Rack middleware, as shown under "Plain Ruby and Rack" (added by `init` in Rails apps).
|
|
384
409
|
|
|
385
410
|
- The request body is captured whether or not the app reads it when the input is rewindable, as it is under Puma, which buffers request bodies. Otherwise what the app reads is captured.
|
|
386
411
|
- A streamed response is observed as the server sends it, without buffering or delaying it. The records are emitted when the body closes; a stream idle for over 60 seconds is closed as incomplete and the rest is dropped.
|
|
@@ -390,13 +415,14 @@ Server bodies need `Foam::Rack::Middleware` after the OpenTelemetry Rack middlew
|
|
|
390
415
|
|
|
391
416
|
Client bodies are captured in `:advanced` by the same hooks that set the headers.
|
|
392
417
|
|
|
393
|
-
- `Net::HTTP`: a request sent through `body_stream
|
|
418
|
+
- `Net::HTTP`: a request is captured whether sent as a string or through `body_stream`. A response read in a block or into an IO is not captured.
|
|
394
419
|
- Faraday: bodies are captured on the Faraday span through Foam's Faraday middleware, whatever the adapter. A request whose body is an IO and a response streamed with `on_data` are not captured.
|
|
395
420
|
- Excon: a request sent with `request_block` and a response received with `response_block` are not captured.
|
|
396
421
|
- HTTPClient: a request whose body is an IO or multipart parts is not captured. A response read in a block is captured.
|
|
397
|
-
- RestClient
|
|
422
|
+
- RestClient and HTTParty: captured on the `Net::HTTP` span beneath them.
|
|
423
|
+
- `http`, `httpx`, and `ethon`: no bodies, since they get no header capture either.
|
|
398
424
|
|
|
399
|
-
TODO(jhartquist): capture streamed bodies
|
|
425
|
+
TODO(jhartquist): capture the streamed bodies listed above. For `Net::HTTP`, prepend on `Net::HTTPResponse#read_body` and tee the block or IO the app passes.
|
|
400
426
|
|
|
401
427
|
## Loggers
|
|
402
428
|
|
|
@@ -414,6 +440,12 @@ If another SDK owns logs, set `disable_log_sending: true` so records do not arri
|
|
|
414
440
|
|
|
415
441
|
Secrets in a log line are masked before export. See Redaction.
|
|
416
442
|
|
|
443
|
+
## AWS Lambda
|
|
444
|
+
|
|
445
|
+
The invocation span carries `faas.trigger` for API Gateway, ALB, SNS, SQS, S3, DynamoDB streams, and EventBridge schedule events.
|
|
446
|
+
|
|
447
|
+
Traces, metrics, and logs are flushed before the handler returns (including failing invocations).
|
|
448
|
+
|
|
417
449
|
## Pre-forking servers and threads
|
|
418
450
|
|
|
419
451
|
TODO(jhartquist): pre-forking servers (Puma in cluster mode, Unicorn, Pitchfork, Resque): whether to call `init` before or after the fork, and what the SDK's batch processors and the metric reader do in the child. Foam registers no fork hook today.
|
|
@@ -431,7 +463,13 @@ end
|
|
|
431
463
|
|
|
432
464
|
Foam masks the values of sensitive keys in everything it exports: span, span event, and span link attributes; log bodies and attributes; metric data point attributes; and resource attributes. A built-in list of credential and personal-data keys always applies, matched by exact name or by a sensitive segment of the key (`legacy_api_key_2`, `stripeToken`). Keys match after normalization: case, `-`, `.`, and camelCase boundaries are folded, so `apiKey`, `api-key`, and `API_KEY` are one key. Header attributes match on the header name. SQL statements arrive from the database instrumentations with every literal replaced with `?` (see Instrumentation) and are redacted like any other string. Captured HTTP bodies are masked by content type with the same keys, with their framing kept. When masking a body fails, the whole body is replaced with `********`.
|
|
433
465
|
|
|
434
|
-
Every other string is scanned as text,
|
|
466
|
+
Every other string is scanned as text, log bodies included.
|
|
467
|
+
|
|
468
|
+
- A string that is a whole JSON document is masked by key at every depth. A list or object under a sensitive key becomes one mask.
|
|
469
|
+
- Any other string has the value after each sensitive key masked. The key may be quoted as JSON writes it or bare before `=`, `:`, or `=>`, in a query string, a header line, or prose. The value ends at the next delimiter, and a quoted URL keeps its closing quote.
|
|
470
|
+
- `url.query` is masked pair by pair. Names such as `code` and `sig` are sensitive only inside a query string.
|
|
471
|
+
|
|
472
|
+
Scanning stops after 1 MiB, and everything past that point is replaced with a single mask. Masking is value-only: the key and the surrounding text remain intact.
|
|
435
473
|
|
|
436
474
|
The `redact` option adds the app's own keys, matched exactly after the same normalization. A value under a built-in or `pii` key becomes `[REDACTED]`. A value under a `secrets` key becomes `********`, followed by its last four characters when it is 12 characters or longer. That tail is kept only where the value is read whole under its key: an attribute, a hash or array, or a whole JSON document. A value found by scanning text or a `url.query` pair becomes `[REDACTED]` whatever its key.
|
|
437
475
|
|
|
@@ -528,7 +566,7 @@ The `bunny`, `rdkafka`, `ruby_kafka`, `racecar`, `sidekiq`, `resque`, `que`, `de
|
|
|
528
566
|
OpenTelemetry packages outside the instrumentation bundle that Foam does not wire yet.
|
|
529
567
|
|
|
530
568
|
- Resource detectors. Foam's resource carries the service identity, the distro, host, and OS attributes, and the process and SDK attributes the OpenTelemetry SDK adds; `OTEL_RESOURCE_ATTRIBUTES` adds to it. The contrib detectors, `opentelemetry-resource-detector-aws`, `-azure`, `-container`, and `-google_cloud_platform`, are not run. The cloud ones probe metadata endpoints, so they belong off the `init` critical path.
|
|
531
|
-
- Propagators. `init`
|
|
569
|
+
- Propagators. `init` registers W3C `tracecontext` and `baggage` when the propagator slot is free and does not read `OTEL_PROPAGATORS`. The contrib propagator gems (`b3`, `b3multi`, `jaeger`, `xray`, `ottrace`) are not dependencies of `foam-otel`; an app that needs one registers it before `init`. `inject_trace_context` and `extract_trace_context` always use the W3C headers.
|
|
532
570
|
- Samplers. `sample_rate` builds a parent-based ratio sampler and `OTEL_TRACES_SAMPLER` is not consulted. `opentelemetry-sampler-xray` is the only contrib sampler; there is no remote sampler for Jaeger.
|
|
533
571
|
|
|
534
572
|
## TODO(jhartquist): Safely
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
# Foam's additions to the aws_lambda instrumentation, prepended on its Wrap module by init
|
|
7
|
+
# after installation:
|
|
8
|
+
# 1. Fills in faas.trigger from the event's shape for the AWS event sources the
|
|
9
|
+
# instrumentation does not derive it for.
|
|
10
|
+
# 2. Flushes Foam's logger provider where the wrapper flushes the tracer and meter providers,
|
|
11
|
+
# so an invocation's log records leave before the handler returns.
|
|
12
|
+
module AwsLambda
|
|
13
|
+
INSTRUMENTATION = "OpenTelemetry::Instrumentation::AwsLambda"
|
|
14
|
+
TRIGGER = "faas.trigger"
|
|
15
|
+
|
|
16
|
+
# A record's event source -> the semantic conventions' trigger for it.
|
|
17
|
+
RECORD_TRIGGERS = {
|
|
18
|
+
"aws:sns" => "pubsub",
|
|
19
|
+
"aws:sqs" => "pubsub",
|
|
20
|
+
"aws:s3" => "datasource",
|
|
21
|
+
"aws:dynamodb" => "datasource"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def self.install
|
|
25
|
+
return unless Instrumentations.instance(INSTRUMENTATION).installed?
|
|
26
|
+
|
|
27
|
+
::OpenTelemetry::Instrumentation::AwsLambda::Wrap.prepend(Wrap)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The trigger the event's shape names, or nil for a shape this does not know. SNS spells
|
|
31
|
+
# its record key EventSource; the other record sources spell it eventSource.
|
|
32
|
+
def self.trigger(event)
|
|
33
|
+
record = event["Records"]&.first
|
|
34
|
+
if record
|
|
35
|
+
RECORD_TRIGGERS[record["eventSource"] || record["EventSource"]]
|
|
36
|
+
elsif event["source"] == "aws.events" && event["detail-type"] == "Scheduled Event"
|
|
37
|
+
"timer"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module Wrap
|
|
42
|
+
def wrap_lambda(
|
|
43
|
+
event:, context:, handler:,
|
|
44
|
+
flush_timeout: ::OpenTelemetry::Instrumentation::AwsLambda::Wrap::DEFAULT_FLUSH_TIMEOUT
|
|
45
|
+
)
|
|
46
|
+
super
|
|
47
|
+
ensure
|
|
48
|
+
# Runs for an error too, so the records of a failed invocation leave before the error
|
|
49
|
+
# reaches the runtime. The timeout is the one the wrapper gives the other providers.
|
|
50
|
+
Safely.call("aws_lambda log flush") do
|
|
51
|
+
State.logger_provider&.force_flush(timeout: flush_timeout)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def otel_attributes(event, context)
|
|
58
|
+
attributes = super
|
|
59
|
+
return attributes if attributes.key?(TRIGGER)
|
|
60
|
+
|
|
61
|
+
trigger = Safely.call("faas.trigger") { AwsLambda.trigger(event) }
|
|
62
|
+
attributes[TRIGGER] = trigger if trigger
|
|
63
|
+
attributes
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -6,10 +6,9 @@ module Foam
|
|
|
6
6
|
module Clients
|
|
7
7
|
# Sets the safe headers on Net::HTTP client spans and, in advanced network capture,
|
|
8
8
|
# captures their request and response bodies. Prepended on Net::HTTP by init unless network
|
|
9
|
-
# capture is off.
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# - a response read in a block or into an IO
|
|
9
|
+
# capture is off. A request body is captured whether set as a String or streamed through
|
|
10
|
+
# body_stream. A response body is captured only when Net::HTTP reads it into a String; a
|
|
11
|
+
# response the app reads itself, in a block or into an IO, is not.
|
|
13
12
|
module NetHttp
|
|
14
13
|
private
|
|
15
14
|
|
|
@@ -35,6 +34,7 @@ module Foam
|
|
|
35
34
|
yield response if block_given?
|
|
36
35
|
end
|
|
37
36
|
ensure
|
|
37
|
+
req.body_stream = req.body_stream.stream if req.body_stream.is_a?(ReadTee)
|
|
38
38
|
request&.finalize(complete: !res.nil?)
|
|
39
39
|
Safely.call("request header capture") do
|
|
40
40
|
BodyCapture.set_header_attributes(span, :request, body_headers(req))
|
|
@@ -44,15 +44,24 @@ module Foam
|
|
|
44
44
|
res
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
#
|
|
47
|
+
# A String body is observed in one piece. A body_stream is wrapped in a ReadTee, which
|
|
48
|
+
# observes each chunk as Net::HTTP copies it to the socket; transport_request unwraps it
|
|
49
|
+
# afterwards. Multipart form data is not captured.
|
|
48
50
|
def capture_request(req, span, body_redactor)
|
|
49
51
|
body = req.body
|
|
50
|
-
|
|
52
|
+
stream = req.body_stream
|
|
53
|
+
return unless body.is_a?(String) || stream.respond_to?(:read)
|
|
51
54
|
|
|
52
55
|
capture = BodyCapture.start(span, body_redactor, side: :client, direction: :request) do
|
|
53
56
|
body_headers(req)
|
|
54
57
|
end
|
|
55
|
-
capture
|
|
58
|
+
return unless capture
|
|
59
|
+
|
|
60
|
+
if body
|
|
61
|
+
capture.observe(body)
|
|
62
|
+
else
|
|
63
|
+
req.body_stream = ReadTee.new(stream, capture)
|
|
64
|
+
end
|
|
56
65
|
capture
|
|
57
66
|
end
|
|
58
67
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
module Clients
|
|
7
|
+
# Wraps a request body_stream so the bytes Net::HTTP sends can be captured without buffering
|
|
8
|
+
# them. Net::HTTP copies the stream to the socket with IO.copy_stream, so the read is the
|
|
9
|
+
# only place to see them.
|
|
10
|
+
class ReadTee
|
|
11
|
+
# The wrapped stream, for putting it back on the request once it is sent.
|
|
12
|
+
attr_reader :stream
|
|
13
|
+
|
|
14
|
+
def initialize(stream, capture)
|
|
15
|
+
@stream = stream
|
|
16
|
+
@capture = capture
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def read(*args)
|
|
20
|
+
bytes = @stream.read(*args)
|
|
21
|
+
@capture.observe(bytes) if bytes
|
|
22
|
+
bytes
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -52,15 +52,24 @@ module Foam
|
|
|
52
52
|
# The log body of every body chunk record; the chunk's text goes in its attributes.
|
|
53
53
|
BODY_CHUNK_EVENT_BODY = "HTTP body chunk"
|
|
54
54
|
|
|
55
|
-
#
|
|
55
|
+
# Replaces a redacted value.
|
|
56
56
|
REDACTED_VALUE = "[REDACTED]"
|
|
57
57
|
|
|
58
|
-
#
|
|
58
|
+
# Replaces a secret: all of it when short or not text, otherwise all but its last four
|
|
59
|
+
# characters. Also stands in for a value whose redaction failed.
|
|
59
60
|
FULL_MASK = "********"
|
|
60
61
|
|
|
61
|
-
#
|
|
62
|
+
# Secrets at least this long keep their last four characters unmasked.
|
|
62
63
|
TAIL_MASK_THRESHOLD = 12
|
|
63
64
|
|
|
65
|
+
# Finished spans Foam's span processor holds before dropping the oldest. The SDK default
|
|
66
|
+
# of 2048 loses spans when a loop finishes more than that before the export thread runs.
|
|
67
|
+
SPAN_QUEUE_SIZE = 4096
|
|
68
|
+
|
|
69
|
+
# Seconds flush and shutdown wait by default. It is the OTLP exporters' own request timeout,
|
|
70
|
+
# and it keeps the at_exit shutdown inside a 30 second deploy grace period.
|
|
71
|
+
LIFECYCLE_TIMEOUT = 10
|
|
72
|
+
|
|
64
73
|
module_function
|
|
65
74
|
|
|
66
75
|
def bearer_headers(token)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foam
|
|
4
|
+
module Otel
|
|
5
|
+
module Internal
|
|
6
|
+
# Runs State.after_fork in every child this process forks, by wrapping Process._fork, the
|
|
7
|
+
# method fork and Process.fork call. Preforking servers init Foam in the master, and a
|
|
8
|
+
# forked child gets the parent's memory but none of its threads, so the periodic metric
|
|
9
|
+
# reader's export thread is gone and the child's metrics would never leave it, and the
|
|
10
|
+
# providers' resource still names the parent process.
|
|
11
|
+
module ForkHooks
|
|
12
|
+
def _fork
|
|
13
|
+
parent = Process.pid
|
|
14
|
+
super.tap { State.after_fork unless Process.pid == parent }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.attach
|
|
18
|
+
Process.singleton_class.prepend(self)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -18,6 +18,10 @@ module Foam
|
|
|
18
18
|
# Every outcome is reported with the same identity, including the ones that stop early.
|
|
19
19
|
identity = options.slice(:name, :environment, :token)
|
|
20
20
|
|
|
21
|
+
# Recorded before the checks, so state names the app after a disabled or failed init too.
|
|
22
|
+
# The token stays out of state on purpose; it is read by health checks and tests.
|
|
23
|
+
State.record_params(options.slice(:name, :environment))
|
|
24
|
+
|
|
21
25
|
# Teams may turn the SDK off in specific environments.
|
|
22
26
|
unless options[:enabled]
|
|
23
27
|
Report.call(severity: :info, message: "SDK disabled", **identity)
|
|
@@ -63,27 +67,39 @@ module Foam
|
|
|
63
67
|
State.take_provider(:logger)&.shutdown(timeout: nil)
|
|
64
68
|
|
|
65
69
|
redactor = Redactor.from_option(options[:redact])
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# The client patches set the safe headers on every client span and, in advanced mode,
|
|
69
|
-
# capture bodies too. Bodies are redacted by the same rules as attributes, so only
|
|
70
|
-
# advanced builds a body redactor; its presence is what switches body capture on.
|
|
71
|
-
body_redactor = BodyRedactor.new(redactor) if options[:network_capture] == :advanced
|
|
72
|
-
install_client_patches unless options[:network_capture] == :off
|
|
73
|
-
|
|
74
|
-
# Recorded last so a failed init leaves the state uninitialized.
|
|
75
|
-
# The token stays out of state on purpose; it is read by health checks and tests.
|
|
76
|
-
State.record_init(
|
|
77
|
-
params: options.slice(:name, :environment),
|
|
78
|
-
tracer_provider: OpenTelemetry.tracer_provider,
|
|
79
|
-
meter_provider: OpenTelemetry.meter_provider,
|
|
80
|
-
logger_provider: OpenTelemetry.logger_provider,
|
|
81
|
-
propagator: OpenTelemetry.propagation,
|
|
82
|
-
instrumentations: Instrumentations.installed(instrumentation_config),
|
|
83
|
-
instrumentation_config: instrumentation_config,
|
|
84
|
-
body_redactor: body_redactor,
|
|
85
|
-
network_capture: options[:network_capture]
|
|
70
|
+
providers, claimed_slots = configure(
|
|
71
|
+
instrumentation_config: instrumentation_config, redactor: redactor, **options
|
|
86
72
|
)
|
|
73
|
+
begin
|
|
74
|
+
ForkHooks.attach
|
|
75
|
+
|
|
76
|
+
# The client patches set the safe headers on every client span and, in advanced mode,
|
|
77
|
+
# capture bodies too. Bodies are redacted by the same rules as attributes, so only
|
|
78
|
+
# advanced builds a body redactor; its presence is what switches body capture on.
|
|
79
|
+
body_redactor = BodyRedactor.new(redactor) if options[:network_capture] == :advanced
|
|
80
|
+
install_client_patches unless options[:network_capture] == :off
|
|
81
|
+
|
|
82
|
+
# Adds faas.trigger for the event sources the aws_lambda instrumentation skips, and flushes
|
|
83
|
+
# Foam's logger provider where it flushes the tracer and meter providers.
|
|
84
|
+
Safely.call("aws_lambda") { AwsLambda.install }
|
|
85
|
+
|
|
86
|
+
# A Rails app cannot add the middleware in config.ru the way a plain Rack app does, so
|
|
87
|
+
# Foam adds it through Rails itself.
|
|
88
|
+
Safely.call("rails middleware") { RailsMiddleware.install } unless options[:network_capture] == :off
|
|
89
|
+
|
|
90
|
+
# Recorded last so a failed init leaves the state uninitialized.
|
|
91
|
+
State.record_init(
|
|
92
|
+
providers: providers,
|
|
93
|
+
claimed_slots: claimed_slots,
|
|
94
|
+
instrumentations: Instrumentations.installed(instrumentation_config),
|
|
95
|
+
instrumentation_config: instrumentation_config,
|
|
96
|
+
body_redactor: body_redactor,
|
|
97
|
+
network_capture: options[:network_capture]
|
|
98
|
+
)
|
|
99
|
+
rescue
|
|
100
|
+
roll_back(providers, claimed_slots)
|
|
101
|
+
raise
|
|
102
|
+
end
|
|
87
103
|
|
|
88
104
|
# Flush whatever the batch processors still hold before the process exits.
|
|
89
105
|
at_exit { Foam.shutdown }
|
|
@@ -94,66 +110,111 @@ module Foam
|
|
|
94
110
|
nil
|
|
95
111
|
end
|
|
96
112
|
|
|
113
|
+
# Builds Foam's pipelines and claims the global slots the API still holds itself. Returns
|
|
114
|
+
# the providers built, by kind, and the slots claimed, by signal.
|
|
97
115
|
def configure(
|
|
98
116
|
token:, name:, environment:, sample_rate:, instrumentation_config:, additional_span_processors:,
|
|
99
117
|
additional_metric_readers:, additional_log_record_processors:, additional_resource_attributes:,
|
|
100
118
|
ignored_outbound_hosts:, redactor:, before_send:, version: nil, **
|
|
101
119
|
)
|
|
102
|
-
# Declared up front so the rescue below can stop whichever
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
log_record_processor = nil
|
|
120
|
+
# Declared up front so the rescue below can stop whichever providers exist when
|
|
121
|
+
# something fails.
|
|
122
|
+
providers = {}
|
|
106
123
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
# The SDK's own switch, honored the way SDK.configure honors it: nothing is installed.
|
|
125
|
+
raise "OTEL_SDK_DISABLED is true" if ENV["OTEL_SDK_DISABLED"] == "true"
|
|
126
|
+
|
|
127
|
+
sampler = sampler(sample_rate, ignored_outbound_hosts)
|
|
128
|
+
resource = OpenTelemetry::SDK::Resources::Resource.default.merge(
|
|
129
|
+
Resource.create(name, environment, version, additional_resource_attributes)
|
|
112
130
|
)
|
|
113
|
-
resource = Resource.create(name, environment, version, additional_resource_attributes)
|
|
114
131
|
before_send = BeforeSend.new(before_send)
|
|
115
132
|
|
|
116
|
-
|
|
133
|
+
# A slot another SDK owns is left in place, and Foam builds no pipeline for that signal,
|
|
134
|
+
# since nothing would feed one. The logger provider is built either way, so log still
|
|
135
|
+
# delivers whoever holds the slot.
|
|
136
|
+
free = Slots::SIGNALS.select { |signal| Slots.free?(signal) }
|
|
137
|
+
(Slots::SIGNALS - free).each do |signal|
|
|
138
|
+
Diagnostics.warn("#{signal} global slot taken by #{Slots.live(signal).class}")
|
|
139
|
+
end
|
|
140
|
+
if free.include?(:traces)
|
|
141
|
+
exporter = Exporters::TraceExporter.new(
|
|
142
|
+
token: token, redactor: redactor, before_send: before_send
|
|
143
|
+
)
|
|
144
|
+
providers[:tracer] = tracer_provider(
|
|
145
|
+
resource, sampler, exporter, additional_span_processors
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
if free.include?(:metrics)
|
|
149
|
+
exporter = Exporters::MetricExporter.new(token: token, redactor: redactor)
|
|
150
|
+
providers[:meter] = meter_provider(resource, exporter, additional_metric_readers)
|
|
151
|
+
end
|
|
152
|
+
exporter = Exporters::LogExporter.new(
|
|
117
153
|
token: token, redactor: redactor, before_send: before_send
|
|
118
154
|
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
token: token, redactor: redactor, before_send: before_send
|
|
155
|
+
providers[:logger] = logger_provider(
|
|
156
|
+
resource, exporter, additional_log_record_processors
|
|
122
157
|
)
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
159
|
+
claimed_slots = {
|
|
160
|
+
traces: providers[:tracer],
|
|
161
|
+
metrics: providers[:meter],
|
|
162
|
+
logs: (providers[:logger] if free.include?(:logs)),
|
|
163
|
+
baggage: (Propagation::PROPAGATOR if free.include?(:baggage))
|
|
164
|
+
}.compact
|
|
165
|
+
claimed_slots.each { |signal, object| Slots.claim(signal, object) }
|
|
166
|
+
|
|
167
|
+
# Installs the bundle and the caller's entries, those whose library is already loaded; the
|
|
168
|
+
# rest are skipped. After the slots, so each one binds its tracer to whoever holds traces.
|
|
169
|
+
OpenTelemetry::Instrumentation.registry.install(
|
|
170
|
+
instrumentation_config.keys, instrumentation_config
|
|
171
|
+
)
|
|
172
|
+
[providers, claimed_slots]
|
|
173
|
+
rescue
|
|
174
|
+
roll_back(providers, claimed_slots)
|
|
175
|
+
raise
|
|
176
|
+
end
|
|
137
177
|
|
|
138
|
-
|
|
139
|
-
|
|
178
|
+
# Frees the slots and stops the providers a failed init built, so the next init starts clean.
|
|
179
|
+
def roll_back(providers, claimed_slots)
|
|
180
|
+
claimed_slots&.each { |signal, object| Slots.release(signal, object) }
|
|
181
|
+
Lifecycle.each(:shutdown, providers.values, timeout: nil)
|
|
182
|
+
end
|
|
140
183
|
|
|
141
|
-
|
|
142
|
-
|
|
184
|
+
# Drops spans headed for an ignored host and samples the rest per trace at sample_rate.
|
|
185
|
+
def sampler(sample_rate, ignored_outbound_hosts)
|
|
186
|
+
IgnoredHosts.new(
|
|
187
|
+
ignored_hosts(ignored_outbound_hosts),
|
|
188
|
+
OpenTelemetry::SDK::Trace::Samplers.parent_based(
|
|
189
|
+
root: OpenTelemetry::SDK::Trace::Samplers.trace_id_ratio_based(sample_rate)
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
end
|
|
143
193
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
194
|
+
def tracer_provider(resource, sampler, exporter, additional)
|
|
195
|
+
provider = OpenTelemetry::SDK::Trace::TracerProvider.new(
|
|
196
|
+
resource: resource, sampler: sampler
|
|
197
|
+
)
|
|
198
|
+
foam = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
199
|
+
exporter,
|
|
200
|
+
max_queue_size: Integer(ENV.fetch("OTEL_BSP_MAX_QUEUE_SIZE", Constants::SPAN_QUEUE_SIZE))
|
|
201
|
+
)
|
|
202
|
+
[foam, *additional].each { |processor| provider.add_span_processor(processor) }
|
|
203
|
+
provider
|
|
204
|
+
end
|
|
147
205
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
206
|
+
def meter_provider(resource, exporter, additional)
|
|
207
|
+
provider = OpenTelemetry::SDK::Metrics::MeterProvider.new(resource: resource)
|
|
208
|
+
foam = OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(exporter: exporter)
|
|
209
|
+
[foam, *additional].each { |reader| provider.add_metric_reader(reader) }
|
|
210
|
+
provider
|
|
211
|
+
end
|
|
151
212
|
|
|
152
|
-
|
|
153
|
-
OpenTelemetry.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
213
|
+
def logger_provider(resource, exporter, additional)
|
|
214
|
+
provider = OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: resource)
|
|
215
|
+
foam = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter)
|
|
216
|
+
[foam, *additional].each { |processor| provider.add_log_record_processor(processor) }
|
|
217
|
+
provider
|
|
157
218
|
end
|
|
158
219
|
|
|
159
220
|
# Net::HTTP is patched without a check, since foam-otel loads it for its own exporters; the
|