foam-otel 3.0.0.alpha.3 → 3.0.0.alpha.4
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 +39 -14
- 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 +119 -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/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 +6 -2
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c7362be48b291f5a1c52aca227430f7ded8a223dfd289108f1bf97b3254b184
|
|
4
|
+
data.tar.gz: b876b8f462eccad1ce7f39e4ced08da44243755c0e615109a89891ef563078d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 157e5f359ca4d872a7106377d0315c767ff6fc624952408ce85383a8b8c2a4a2e0de62b97c15fc3d8fd261ea09b276b79c0b1b0c4205105a6ab44b45043ea5ab
|
|
7
|
+
data.tar.gz: c731cca87d8ccf41524ae89da935e8878efb45d3d7ae11c84b7172ddc490ac579da0cd363079fbce6ed8dd24750fa939fa65d30fc2c8611ec8f1b0ce341a2b9d
|
data/README.md
CHANGED
|
@@ -89,6 +89,8 @@ Every method lives on the `Foam` module. Other than the ingest factories, none o
|
|
|
89
89
|
|
|
90
90
|
Sets up tracing, metrics, logs, automatic instrumentation, W3C `tracecontext` and `baggage` propagation, and OTLP export to Foam.
|
|
91
91
|
|
|
92
|
+
`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.
|
|
93
|
+
|
|
92
94
|
Call this once. Use it when Foam should run OpenTelemetry for the process.
|
|
93
95
|
|
|
94
96
|
```ruby
|
|
@@ -133,7 +135,7 @@ Foam.init(
|
|
|
133
135
|
- `version` - the service version, sent as `service.version` when given.
|
|
134
136
|
- `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
137
|
- `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.
|
|
138
|
+
- `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
139
|
- `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
140
|
- `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
141
|
- `disable_log_sending` - `true` stops shipping the app's `Logger` lines to Foam. `Foam.log` is unaffected. See Loggers.
|
|
@@ -143,11 +145,19 @@ Foam.init(
|
|
|
143
145
|
|
|
144
146
|
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
147
|
|
|
146
|
-
Every signal carries a resource with
|
|
148
|
+
Every signal carries a resource with these attributes:
|
|
149
|
+
|
|
150
|
+
- The service identity from the options above: `service.name`, `deployment.environment.name`, and `service.version` when given.
|
|
151
|
+
- `service.instance.id`, a UUID made for the process at `init`.
|
|
152
|
+
- `telemetry.distro.name` and `telemetry.distro.version`, naming this gem.
|
|
153
|
+
- `host.name`, `host.arch`, `host.id` when it can be read, `os.type`, and `os.version`.
|
|
154
|
+
- The process and SDK attributes the OpenTelemetry SDK adds.
|
|
155
|
+
|
|
156
|
+
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
157
|
|
|
148
158
|
`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
159
|
|
|
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.
|
|
160
|
+
`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
161
|
|
|
152
162
|
### `set_endpoint(url)`
|
|
153
163
|
|
|
@@ -229,7 +239,7 @@ OpenTelemetry::Context.with_current(context) do
|
|
|
229
239
|
end
|
|
230
240
|
```
|
|
231
241
|
|
|
232
|
-
|
|
242
|
+
`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
243
|
|
|
234
244
|
### `set_baggage(key, value)`
|
|
235
245
|
|
|
@@ -287,9 +297,9 @@ rescue PaymentError => e
|
|
|
287
297
|
end
|
|
288
298
|
```
|
|
289
299
|
|
|
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.
|
|
300
|
+
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
301
|
|
|
292
|
-
### `flush(timeout:
|
|
302
|
+
### `flush(timeout: 10)`
|
|
293
303
|
|
|
294
304
|
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
305
|
|
|
@@ -299,14 +309,16 @@ Foam.flush(timeout: 5)
|
|
|
299
309
|
|
|
300
310
|
`timeout` is in seconds and bounds the whole call. A provider that runs out of time is reported on stderr.
|
|
301
311
|
|
|
302
|
-
### `shutdown(timeout:
|
|
312
|
+
### `shutdown(timeout: 10)`
|
|
303
313
|
|
|
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.
|
|
314
|
+
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
315
|
|
|
306
316
|
`timeout` is in seconds and bounds the whole call. A provider that runs out of time is reported on stderr.
|
|
307
317
|
|
|
308
318
|
`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
319
|
|
|
320
|
+
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.
|
|
321
|
+
|
|
310
322
|
### `state`
|
|
311
323
|
|
|
312
324
|
Returns whether Foam initialized, which instrumentations registered, which signals have an export path, and the identity `init` was given.
|
|
@@ -342,7 +354,7 @@ Each `signals` value names the source of the Foam export path for that signal:
|
|
|
342
354
|
- `:local` - logs only: another SDK owns the global provider and Foam keeps its own so `log` still delivers.
|
|
343
355
|
- `:none` - no Foam export path.
|
|
344
356
|
|
|
345
|
-
`:global` is checked against the live OpenTelemetry global
|
|
357
|
+
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
358
|
|
|
347
359
|
## Custom spans and other OpenTelemetry APIs
|
|
348
360
|
|
|
@@ -390,13 +402,14 @@ Server bodies need `Foam::Rack::Middleware` after the OpenTelemetry Rack middlew
|
|
|
390
402
|
|
|
391
403
|
Client bodies are captured in `:advanced` by the same hooks that set the headers.
|
|
392
404
|
|
|
393
|
-
- `Net::HTTP`: a request sent through `body_stream
|
|
405
|
+
- `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
406
|
- 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
407
|
- Excon: a request sent with `request_block` and a response received with `response_block` are not captured.
|
|
396
408
|
- HTTPClient: a request whose body is an IO or multipart parts is not captured. A response read in a block is captured.
|
|
397
|
-
- RestClient
|
|
409
|
+
- RestClient and HTTParty: captured on the `Net::HTTP` span beneath them.
|
|
410
|
+
- `http`, `httpx`, and `ethon`: no bodies, since they get no header capture either.
|
|
398
411
|
|
|
399
|
-
TODO(jhartquist): capture streamed bodies
|
|
412
|
+
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
413
|
|
|
401
414
|
## Loggers
|
|
402
415
|
|
|
@@ -414,6 +427,12 @@ If another SDK owns logs, set `disable_log_sending: true` so records do not arri
|
|
|
414
427
|
|
|
415
428
|
Secrets in a log line are masked before export. See Redaction.
|
|
416
429
|
|
|
430
|
+
## AWS Lambda
|
|
431
|
+
|
|
432
|
+
The invocation span carries `faas.trigger` for API Gateway, ALB, SNS, SQS, S3, DynamoDB streams, and EventBridge schedule events.
|
|
433
|
+
|
|
434
|
+
Traces, metrics, and logs are flushed before the handler returns (including failing invocations).
|
|
435
|
+
|
|
417
436
|
## Pre-forking servers and threads
|
|
418
437
|
|
|
419
438
|
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 +450,13 @@ end
|
|
|
431
450
|
|
|
432
451
|
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
452
|
|
|
434
|
-
Every other string is scanned as text,
|
|
453
|
+
Every other string is scanned as text, log bodies included.
|
|
454
|
+
|
|
455
|
+
- 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.
|
|
456
|
+
- 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.
|
|
457
|
+
- `url.query` is masked pair by pair. Names such as `code` and `sig` are sensitive only inside a query string.
|
|
458
|
+
|
|
459
|
+
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
460
|
|
|
436
461
|
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
462
|
|
|
@@ -528,7 +553,7 @@ The `bunny`, `rdkafka`, `ruby_kafka`, `racecar`, `sidekiq`, `resque`, `que`, `de
|
|
|
528
553
|
OpenTelemetry packages outside the instrumentation bundle that Foam does not wire yet.
|
|
529
554
|
|
|
530
555
|
- 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`
|
|
556
|
+
- 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
557
|
- 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
558
|
|
|
534
559
|
## 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,35 @@ 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
|
+
# Recorded last so a failed init leaves the state uninitialized.
|
|
87
|
+
State.record_init(
|
|
88
|
+
providers: providers,
|
|
89
|
+
claimed_slots: claimed_slots,
|
|
90
|
+
instrumentations: Instrumentations.installed(instrumentation_config),
|
|
91
|
+
instrumentation_config: instrumentation_config,
|
|
92
|
+
body_redactor: body_redactor,
|
|
93
|
+
network_capture: options[:network_capture]
|
|
94
|
+
)
|
|
95
|
+
rescue
|
|
96
|
+
roll_back(providers, claimed_slots)
|
|
97
|
+
raise
|
|
98
|
+
end
|
|
87
99
|
|
|
88
100
|
# Flush whatever the batch processors still hold before the process exits.
|
|
89
101
|
at_exit { Foam.shutdown }
|
|
@@ -94,66 +106,111 @@ module Foam
|
|
|
94
106
|
nil
|
|
95
107
|
end
|
|
96
108
|
|
|
109
|
+
# Builds Foam's pipelines and claims the global slots the API still holds itself. Returns
|
|
110
|
+
# the providers built, by kind, and the slots claimed, by signal.
|
|
97
111
|
def configure(
|
|
98
112
|
token:, name:, environment:, sample_rate:, instrumentation_config:, additional_span_processors:,
|
|
99
113
|
additional_metric_readers:, additional_log_record_processors:, additional_resource_attributes:,
|
|
100
114
|
ignored_outbound_hosts:, redactor:, before_send:, version: nil, **
|
|
101
115
|
)
|
|
102
|
-
# Declared up front so the rescue below can stop whichever
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
log_record_processor = nil
|
|
116
|
+
# Declared up front so the rescue below can stop whichever providers exist when
|
|
117
|
+
# something fails.
|
|
118
|
+
providers = {}
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
# The SDK's own switch, honored the way SDK.configure honors it: nothing is installed.
|
|
121
|
+
raise "OTEL_SDK_DISABLED is true" if ENV["OTEL_SDK_DISABLED"] == "true"
|
|
122
|
+
|
|
123
|
+
sampler = sampler(sample_rate, ignored_outbound_hosts)
|
|
124
|
+
resource = OpenTelemetry::SDK::Resources::Resource.default.merge(
|
|
125
|
+
Resource.create(name, environment, version, additional_resource_attributes)
|
|
112
126
|
)
|
|
113
|
-
resource = Resource.create(name, environment, version, additional_resource_attributes)
|
|
114
127
|
before_send = BeforeSend.new(before_send)
|
|
115
128
|
|
|
116
|
-
|
|
129
|
+
# A slot another SDK owns is left in place, and Foam builds no pipeline for that signal,
|
|
130
|
+
# since nothing would feed one. The logger provider is built either way, so log still
|
|
131
|
+
# delivers whoever holds the slot.
|
|
132
|
+
free = Slots::SIGNALS.select { |signal| Slots.free?(signal) }
|
|
133
|
+
(Slots::SIGNALS - free).each do |signal|
|
|
134
|
+
Diagnostics.warn("#{signal} global slot taken by #{Slots.live(signal).class}")
|
|
135
|
+
end
|
|
136
|
+
if free.include?(:traces)
|
|
137
|
+
exporter = Exporters::TraceExporter.new(
|
|
138
|
+
token: token, redactor: redactor, before_send: before_send
|
|
139
|
+
)
|
|
140
|
+
providers[:tracer] = tracer_provider(
|
|
141
|
+
resource, sampler, exporter, additional_span_processors
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
if free.include?(:metrics)
|
|
145
|
+
exporter = Exporters::MetricExporter.new(token: token, redactor: redactor)
|
|
146
|
+
providers[:meter] = meter_provider(resource, exporter, additional_metric_readers)
|
|
147
|
+
end
|
|
148
|
+
exporter = Exporters::LogExporter.new(
|
|
117
149
|
token: token, redactor: redactor, before_send: before_send
|
|
118
150
|
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
token: token, redactor: redactor, before_send: before_send
|
|
151
|
+
providers[:logger] = logger_provider(
|
|
152
|
+
resource, exporter, additional_log_record_processors
|
|
122
153
|
)
|
|
123
154
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
claimed_slots = {
|
|
156
|
+
traces: providers[:tracer],
|
|
157
|
+
metrics: providers[:meter],
|
|
158
|
+
logs: (providers[:logger] if free.include?(:logs)),
|
|
159
|
+
baggage: (Propagation::PROPAGATOR if free.include?(:baggage))
|
|
160
|
+
}.compact
|
|
161
|
+
claimed_slots.each { |signal, object| Slots.claim(signal, object) }
|
|
162
|
+
|
|
163
|
+
# Installs the bundle and the caller's entries, those whose library is already loaded; the
|
|
164
|
+
# rest are skipped. After the slots, so each one binds its tracer to whoever holds traces.
|
|
165
|
+
OpenTelemetry::Instrumentation.registry.install(
|
|
166
|
+
instrumentation_config.keys, instrumentation_config
|
|
167
|
+
)
|
|
168
|
+
[providers, claimed_slots]
|
|
169
|
+
rescue
|
|
170
|
+
roll_back(providers, claimed_slots)
|
|
171
|
+
raise
|
|
172
|
+
end
|
|
137
173
|
|
|
138
|
-
|
|
139
|
-
|
|
174
|
+
# Frees the slots and stops the providers a failed init built, so the next init starts clean.
|
|
175
|
+
def roll_back(providers, claimed_slots)
|
|
176
|
+
claimed_slots&.each { |signal, object| Slots.release(signal, object) }
|
|
177
|
+
Lifecycle.each(:shutdown, providers.values, timeout: nil)
|
|
178
|
+
end
|
|
140
179
|
|
|
141
|
-
|
|
142
|
-
|
|
180
|
+
# Drops spans headed for an ignored host and samples the rest per trace at sample_rate.
|
|
181
|
+
def sampler(sample_rate, ignored_outbound_hosts)
|
|
182
|
+
IgnoredHosts.new(
|
|
183
|
+
ignored_hosts(ignored_outbound_hosts),
|
|
184
|
+
OpenTelemetry::SDK::Trace::Samplers.parent_based(
|
|
185
|
+
root: OpenTelemetry::SDK::Trace::Samplers.trace_id_ratio_based(sample_rate)
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
end
|
|
143
189
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
190
|
+
def tracer_provider(resource, sampler, exporter, additional)
|
|
191
|
+
provider = OpenTelemetry::SDK::Trace::TracerProvider.new(
|
|
192
|
+
resource: resource, sampler: sampler
|
|
193
|
+
)
|
|
194
|
+
foam = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
195
|
+
exporter,
|
|
196
|
+
max_queue_size: Integer(ENV.fetch("OTEL_BSP_MAX_QUEUE_SIZE", Constants::SPAN_QUEUE_SIZE))
|
|
197
|
+
)
|
|
198
|
+
[foam, *additional].each { |processor| provider.add_span_processor(processor) }
|
|
199
|
+
provider
|
|
200
|
+
end
|
|
147
201
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
202
|
+
def meter_provider(resource, exporter, additional)
|
|
203
|
+
provider = OpenTelemetry::SDK::Metrics::MeterProvider.new(resource: resource)
|
|
204
|
+
foam = OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(exporter: exporter)
|
|
205
|
+
[foam, *additional].each { |reader| provider.add_metric_reader(reader) }
|
|
206
|
+
provider
|
|
207
|
+
end
|
|
151
208
|
|
|
152
|
-
|
|
153
|
-
OpenTelemetry.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
209
|
+
def logger_provider(resource, exporter, additional)
|
|
210
|
+
provider = OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: resource)
|
|
211
|
+
foam = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter)
|
|
212
|
+
[foam, *additional].each { |processor| provider.add_log_record_processor(processor) }
|
|
213
|
+
provider
|
|
157
214
|
end
|
|
158
215
|
|
|
159
216
|
# Net::HTTP is patched without a check, since foam-otel loads it for its own exporters; the
|
|
@@ -88,9 +88,9 @@ module Foam
|
|
|
88
88
|
|
|
89
89
|
module_function
|
|
90
90
|
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
# instrumentation is not loaded
|
|
91
|
+
# The instrumentation configs init installs, by name: the bundle, then the caller's entries,
|
|
92
|
+
# which override it. Disabling log sending disables the logger bridge. An entry naming an
|
|
93
|
+
# instrumentation that is not loaded fails init before anything is built.
|
|
94
94
|
def config_map(additional, disable_log_sending: false)
|
|
95
95
|
bundle = TABLE.keys.to_h { |name| [name, nil] }.merge(CONFIG)
|
|
96
96
|
bundle[LOGGER] = {enabled: false} if disable_log_sending
|
|
@@ -19,13 +19,21 @@ module Foam
|
|
|
19
19
|
each(:shutdown, providers, timeout: timeout)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
# Providers, processors, and readers all flush and shut down the same way; each
|
|
23
|
-
# one
|
|
22
|
+
# Providers, processors, and readers all flush and shut down the same way; each in its own
|
|
23
|
+
# thread, so one that hangs or fails holds up neither the others nor the caller. The caller is
|
|
24
|
+
# back within one shared timeout; a thread still running then finishes on its own.
|
|
24
25
|
def each(operation, components, timeout:)
|
|
25
26
|
start = OpenTelemetry::Common::Utilities.timeout_timestamp
|
|
26
|
-
components.compact.
|
|
27
|
+
threads = components.compact.to_h do |component|
|
|
28
|
+
[component, Thread.new do
|
|
29
|
+
# The joining thread reports the failure, so the thread itself stays quiet.
|
|
30
|
+
Thread.current.report_on_exception = false
|
|
31
|
+
component.public_send(operation, timeout: timeout)
|
|
32
|
+
end]
|
|
33
|
+
end
|
|
34
|
+
threads.each do |component, thread|
|
|
27
35
|
remaining = OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start)
|
|
28
|
-
code =
|
|
36
|
+
code = thread.join(remaining) ? thread.value : TIMEOUT
|
|
29
37
|
|
|
30
38
|
# Failures are logged by the exporter or provider themselves; a timeout is silent upstream.
|
|
31
39
|
Diagnostics.warn("#{operation}: #{component.class.name} timed out") if code == TIMEOUT
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "net/http"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "timeout"
|
|
5
7
|
require "opentelemetry/common"
|
|
6
8
|
|
|
7
9
|
module Foam
|
|
@@ -12,24 +14,47 @@ module Foam
|
|
|
12
14
|
# correlation, one request per record.
|
|
13
15
|
module Otlp
|
|
14
16
|
TIMEOUT = 2
|
|
17
|
+
# Seconds the caller spends resolving the endpoint host before a send.
|
|
18
|
+
RESOLVE_TIMEOUT = 1
|
|
19
|
+
|
|
20
|
+
@pending = []
|
|
21
|
+
@mutex = Mutex.new
|
|
15
22
|
|
|
16
23
|
module_function
|
|
17
24
|
|
|
18
25
|
# Sends in the background so the caller never waits on the network; delivery is best effort.
|
|
19
26
|
def send_log(token:, resource_attributes:, scope_name:, severity:, body:)
|
|
20
27
|
request = JSON.generate(log_request(resource_attributes, scope_name, severity, body))
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
uri = URI(Endpoint.url(Constants::LOGS_PATH))
|
|
29
|
+
address = resolve(uri)
|
|
30
|
+
thread = Thread.new do
|
|
23
31
|
# Untraced so the request never becomes a span once Net::HTTP is instrumented, and
|
|
24
32
|
# marked as Foam's own so the client patches leave it alone.
|
|
25
33
|
OwnRequest.mark do
|
|
26
|
-
OpenTelemetry::Common::Utilities.untraced { post(
|
|
34
|
+
OpenTelemetry::Common::Utilities.untraced { post(uri, address, request, token) }
|
|
27
35
|
end
|
|
28
36
|
rescue
|
|
29
37
|
nil
|
|
30
38
|
end
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
@pending.select!(&:alive?)
|
|
41
|
+
@pending << thread
|
|
42
|
+
end
|
|
43
|
+
thread
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Waits for the sends still in flight, each with what is left of one shared timeout.
|
|
47
|
+
def drain(timeout: TIMEOUT)
|
|
48
|
+
start = OpenTelemetry::Common::Utilities.timeout_timestamp
|
|
49
|
+
@mutex.synchronize { @pending.dup }.each do |thread|
|
|
50
|
+
thread.join(OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start))
|
|
51
|
+
end
|
|
52
|
+
nil
|
|
31
53
|
end
|
|
32
54
|
|
|
55
|
+
# A process that exits right after init would otherwise lose its report.
|
|
56
|
+
at_exit { drain }
|
|
57
|
+
|
|
33
58
|
def log_request(resource_attributes, scope_name, severity, body)
|
|
34
59
|
{
|
|
35
60
|
resourceLogs: [{
|
|
@@ -49,13 +74,25 @@ module Foam
|
|
|
49
74
|
}
|
|
50
75
|
end
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
# Resolve on the calling thread. On Ruby 3.3, forking while a background thread is mid
|
|
78
|
+
# lookup leaves the child's resolver deadlocked.
|
|
79
|
+
def resolve(uri)
|
|
80
|
+
Timeout.timeout(RESOLVE_TIMEOUT) do
|
|
81
|
+
Addrinfo.getaddrinfo(uri.hostname, uri.port, nil, :STREAM).first.ip_address
|
|
82
|
+
end
|
|
83
|
+
rescue
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def post(uri, address, request, token)
|
|
54
88
|
headers = Constants.bearer_headers(token).merge("Content-Type" => "application/json")
|
|
55
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
89
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", ipaddr: address,
|
|
56
90
|
open_timeout: TIMEOUT, read_timeout: TIMEOUT, write_timeout: TIMEOUT) do |http|
|
|
57
91
|
http.post(uri.path, request, headers)
|
|
58
92
|
end
|
|
93
|
+
rescue SystemCallError, Net::OpenTimeout
|
|
94
|
+
# The address is the first Net::HTTP would try itself; a retry without it tries the rest.
|
|
95
|
+
address ? post(uri, nil, request, token) : raise
|
|
59
96
|
end
|
|
60
97
|
end
|
|
61
98
|
end
|
|
@@ -40,6 +40,9 @@ module Foam
|
|
|
40
40
|
# keeps its quotes or runs to the next delimiter; an auth scheme word like Bearer is part of it.
|
|
41
41
|
JSON_KEY = /"((?:[^"\\\n]|\\.)*+)"(\s*(?:=>|:)\s*)/
|
|
42
42
|
BARE_KEY = /(?<![\w.%])([A-Za-z_][\w.\-%]*+)([ \t]*(?:=>|={1,3}|:)[ \t]*)/
|
|
43
|
+
# Both key shapes in one pattern, so one pass visits every key in source order. Groups 1 and 2
|
|
44
|
+
# are a quoted key and its separator, groups 3 and 4 a bare one.
|
|
45
|
+
KEY = Regexp.union(JSON_KEY, BARE_KEY)
|
|
43
46
|
QUOTED_VALUE = /"(?:[^"\\\n]|\\.)*+"?|'(?:[^'\\\n]|\\.)*+'?/
|
|
44
47
|
BARE_VALUE = /(?![\[{(])[^\s&"'<>,;)\]}]+/
|
|
45
48
|
VALUE = /(?:(?:bearer|basic|digest|token)[ \t]+)?(?:#{QUOTED_VALUE}|#{BARE_VALUE})/i
|
|
@@ -126,7 +129,9 @@ module Foam
|
|
|
126
129
|
|
|
127
130
|
# [REDACTED] for a built-in or pii key; a secret key keeps the last four characters of a String,
|
|
128
131
|
# Symbol, or number of 12 or more characters, so a value stays recognizable without being usable.
|
|
132
|
+
# nil stays nil: there is no value to protect.
|
|
129
133
|
def mask(kind, value)
|
|
134
|
+
return if value.nil?
|
|
130
135
|
return Constants::REDACTED_VALUE if value == Constants::REDACTED_VALUE
|
|
131
136
|
return Constants::REDACTED_VALUE unless kind == :secret
|
|
132
137
|
return Constants::FULL_MASK unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Numeric)
|
|
@@ -211,8 +216,7 @@ module Foam
|
|
|
211
216
|
mask_kind(name) || (:floor if HTTP_HEADERS.include?(normalize_key(name)))
|
|
212
217
|
end
|
|
213
218
|
|
|
214
|
-
# Arrays are masked element by element so their length survives
|
|
215
|
-
# so the export shows the key was set.
|
|
219
|
+
# Arrays are masked element by element so their length survives.
|
|
216
220
|
def mask_elements(kind, value)
|
|
217
221
|
value.is_a?(Array) ? value.map { |item| mask(kind, item) } : mask(kind, value)
|
|
218
222
|
end
|
|
@@ -232,20 +236,21 @@ module Foam
|
|
|
232
236
|
end
|
|
233
237
|
|
|
234
238
|
# Masks the value after each sensitive key, quoted as JSON writes it or bare. Past 1 MiB the rest
|
|
235
|
-
# collapses into one mask, which bounds the work on a hostile line
|
|
239
|
+
# collapses into one mask, which bounds the work on a hostile line; a value the cut falls
|
|
240
|
+
# inside already ends in that mask.
|
|
236
241
|
def scan_text(bytes)
|
|
237
|
-
return scan_pairs(
|
|
242
|
+
return scan_pairs(bytes) if bytes.bytesize <= SCAN_BYTE_CAP
|
|
238
243
|
|
|
239
|
-
head = bytes.byteslice(0, SCAN_BYTE_CAP)
|
|
240
|
-
|
|
244
|
+
head = scan_pairs(bytes.byteslice(0, SCAN_BYTE_CAP))
|
|
245
|
+
head.end_with?(Constants::REDACTED_VALUE) ? head : head << Constants::REDACTED_VALUE
|
|
241
246
|
end
|
|
242
247
|
|
|
243
|
-
# Finds each key
|
|
244
|
-
def scan_pairs(bytes
|
|
248
|
+
# Finds each key, quoted or bare, and masks the value after it when the key is sensitive.
|
|
249
|
+
def scan_pairs(bytes)
|
|
245
250
|
scanner = StringScanner.new(bytes, fixed_anchor: true)
|
|
246
251
|
out = nil
|
|
247
252
|
last = 0
|
|
248
|
-
while scanner.scan_until(
|
|
253
|
+
while scanner.scan_until(KEY)
|
|
249
254
|
next unless masked_key?(scanner, bytes)
|
|
250
255
|
|
|
251
256
|
start = scanner.pos
|
|
@@ -260,13 +265,13 @@ module Foam
|
|
|
260
265
|
# A key is masked on its name, or on a query-only name when it follows ?, &, or # with a bare =
|
|
261
266
|
# after it: code, sig and the like are credentials only in a query string.
|
|
262
267
|
def masked_key?(scanner, bytes)
|
|
263
|
-
key = decode_key(scanner[1])
|
|
268
|
+
key = decode_key(scanner[1] || scanner[3])
|
|
264
269
|
return true if mask_kind(key)
|
|
265
270
|
|
|
266
271
|
start = scanner.pos - scanner.matched_size
|
|
267
272
|
return false unless start > 0 && QUERY_CONTEXT.include?(bytes.getbyte(start - 1))
|
|
268
273
|
|
|
269
|
-
scanner[
|
|
274
|
+
scanner[4] == "=" && query_key?(key)
|
|
270
275
|
end
|
|
271
276
|
|
|
272
277
|
# The mask in the quotes the value came with; a quote left open stays open.
|
|
@@ -2,27 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
require "etc"
|
|
4
4
|
require "rbconfig"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
require "socket"
|
|
6
7
|
require "opentelemetry/sdk"
|
|
7
8
|
require "opentelemetry-semantic_conventions"
|
|
8
9
|
require "opentelemetry/semconv/incubating/host"
|
|
9
10
|
require "opentelemetry/semconv/incubating/os"
|
|
11
|
+
require "opentelemetry/semconv/incubating/process"
|
|
10
12
|
|
|
11
13
|
module Foam
|
|
12
14
|
module Otel
|
|
13
15
|
module Internal
|
|
14
16
|
# A resource is the set of attributes describing the process that produced the telemetry:
|
|
15
17
|
# which service, environment, host, and OS it came from. Every span, log, and metric
|
|
16
|
-
# carries it, and Foam reads the identity from it. This builds Foam's part;
|
|
17
|
-
#
|
|
18
|
+
# carries it, and Foam reads the identity from it. This builds Foam's part; init
|
|
19
|
+
# merges it over the SDK default, which already holds the process attributes
|
|
18
20
|
# and OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES.
|
|
19
21
|
module Resource
|
|
20
22
|
HOST = OpenTelemetry::SemConv::Incubating::HOST
|
|
21
23
|
OS = OpenTelemetry::SemConv::Incubating::OS
|
|
24
|
+
PROCESS = OpenTelemetry::SemConv::Incubating::PROCESS
|
|
22
25
|
|
|
23
26
|
IDENTITY_ATTRIBUTES = [
|
|
24
27
|
OpenTelemetry::SemConv::SERVICE::SERVICE_NAME,
|
|
25
28
|
OpenTelemetry::SemConv::SERVICE::SERVICE_VERSION,
|
|
29
|
+
OpenTelemetry::SemConv::SERVICE::SERVICE_INSTANCE_ID,
|
|
26
30
|
OpenTelemetry::SemConv::DEPLOYMENT::DEPLOYMENT_ENVIRONMENT_NAME
|
|
27
31
|
].freeze
|
|
28
32
|
|
|
@@ -49,11 +53,12 @@ module Foam
|
|
|
49
53
|
|
|
50
54
|
module_function
|
|
51
55
|
|
|
52
|
-
# Foam's identity
|
|
56
|
+
# Foam's identity and process merge last, so they win over the caller's attributes and env.
|
|
53
57
|
def create(name, environment, version, additional_attributes)
|
|
54
58
|
attributes = host_and_os
|
|
55
59
|
.merge(without_identity(additional_attributes))
|
|
56
60
|
.merge(identity(name, environment, version))
|
|
61
|
+
.merge(process)
|
|
57
62
|
OpenTelemetry::SDK::Resources::Resource.create(attributes)
|
|
58
63
|
end
|
|
59
64
|
|
|
@@ -67,11 +72,29 @@ module Foam
|
|
|
67
72
|
}.compact
|
|
68
73
|
end
|
|
69
74
|
|
|
75
|
+
# The attributes that name the process rather than the service: its pid and an instance id
|
|
76
|
+
# Foam generates, since nothing in the environment tells two runs of one service apart.
|
|
77
|
+
# A forked child inherits its parent's, so it sets both again.
|
|
78
|
+
def process
|
|
79
|
+
{
|
|
80
|
+
PROCESS::PROCESS_PID => Process.pid,
|
|
81
|
+
OpenTelemetry::SemConv::SERVICE::SERVICE_INSTANCE_ID => SecureRandom.uuid
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Sets the process attributes again on the resource of each provider, for the process
|
|
86
|
+
# that holds the providers now. A provider takes its resource once, at construction.
|
|
87
|
+
def refresh(providers)
|
|
88
|
+
update = OpenTelemetry::SDK::Resources::Resource.create(process)
|
|
89
|
+
providers.each do |provider|
|
|
90
|
+
resource = provider.instance_variable_get(:@resource).merge(update)
|
|
91
|
+
provider.instance_variable_set(:@resource, resource)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
70
95
|
def without_identity(attributes)
|
|
71
96
|
(attributes.keys & IDENTITY_ATTRIBUTES).each do |key|
|
|
72
|
-
Diagnostics.warn(
|
|
73
|
-
"additional_resource_attributes #{key} is set from the init options; ignored"
|
|
74
|
-
)
|
|
97
|
+
Diagnostics.warn("additional_resource_attributes #{key} is set by Foam; ignored")
|
|
75
98
|
end
|
|
76
99
|
attributes.except(*IDENTITY_ATTRIBUTES)
|
|
77
100
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opentelemetry/sdk"
|
|
4
|
+
require "opentelemetry-logs-sdk"
|
|
5
|
+
require "opentelemetry-metrics-sdk"
|
|
6
|
+
|
|
7
|
+
module Foam
|
|
8
|
+
module Otel
|
|
9
|
+
module Internal
|
|
10
|
+
# The OpenTelemetry globals an SDK registers itself in: the tracer, meter, and logger
|
|
11
|
+
# providers and the propagator. A slot is free while it holds the API's own no-op.
|
|
12
|
+
module Slots
|
|
13
|
+
ACCESSORS = {
|
|
14
|
+
traces: :tracer_provider,
|
|
15
|
+
metrics: :meter_provider,
|
|
16
|
+
logs: :logger_provider,
|
|
17
|
+
baggage: :propagation
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
SIGNALS = ACCESSORS.keys.freeze
|
|
21
|
+
|
|
22
|
+
DEFAULTS = {
|
|
23
|
+
traces: OpenTelemetry::Internal::ProxyTracerProvider,
|
|
24
|
+
metrics: OpenTelemetry::Internal::ProxyMeterProvider,
|
|
25
|
+
logs: OpenTelemetry::Internal::ProxyLoggerProvider,
|
|
26
|
+
baggage: OpenTelemetry::Context::Propagation::NoopTextMapPropagator
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def live(signal)
|
|
32
|
+
OpenTelemetry.public_send(ACCESSORS.fetch(signal))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def free?(signal)
|
|
36
|
+
live(signal).instance_of?(DEFAULTS.fetch(signal))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def claim(signal, object)
|
|
40
|
+
OpenTelemetry.public_send(:"#{ACCESSORS.fetch(signal)}=", object)
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Puts the API's no-op back while the slot still holds object, so the next registration,
|
|
45
|
+
# or a second init, finds the slot free. A slot another SDK claimed since is left in place.
|
|
46
|
+
def release(signal, object)
|
|
47
|
+
claim(signal, DEFAULTS.fetch(signal).new) if live(signal).equal?(object)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -12,7 +12,7 @@ module Foam
|
|
|
12
12
|
@initialized = false
|
|
13
13
|
@signals = NO_SIGNALS
|
|
14
14
|
@providers = {}
|
|
15
|
-
@
|
|
15
|
+
@claimed_slots = {}
|
|
16
16
|
@taken = []
|
|
17
17
|
@instrumentations = []
|
|
18
18
|
@instrumentation_config = {}
|
|
@@ -39,19 +39,32 @@ module Foam
|
|
|
39
39
|
@signals[signal] != :none
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Records the app's name and environment at the start of init, so state names the app
|
|
43
|
+
# even when init stops early.
|
|
44
|
+
def record_params(params)
|
|
45
|
+
@mutex.synchronize { @params = params unless @initialized }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Records a finished init and marks the state initialized. providers holds what init
|
|
49
|
+
# built, keyed :tracer, :meter and :logger, for flush and shutdown to run over.
|
|
50
|
+
# claimed_slots holds the object init placed in each global slot, keyed by signal; a
|
|
51
|
+
# signal reports :global only while that object is still in its slot.
|
|
42
52
|
def record_init(
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
providers:, claimed_slots:, instrumentations:, instrumentation_config:,
|
|
54
|
+
body_redactor:, network_capture:
|
|
45
55
|
)
|
|
46
56
|
@mutex.synchronize do
|
|
47
|
-
@
|
|
48
|
-
@
|
|
49
|
-
@propagator = propagator
|
|
57
|
+
@providers = providers
|
|
58
|
+
@claimed_slots = claimed_slots
|
|
50
59
|
@instrumentations = instrumentations
|
|
51
60
|
@instrumentation_config = instrumentation_config
|
|
52
61
|
@body_redactor = body_redactor
|
|
53
62
|
@network_capture = network_capture
|
|
54
|
-
@signals = @signals.merge(
|
|
63
|
+
@signals = @signals.merge(
|
|
64
|
+
Slots::SIGNALS.to_h do |signal|
|
|
65
|
+
[signal, claimed_slots.key?(signal) ? :global : without_slot(signal)]
|
|
66
|
+
end
|
|
67
|
+
)
|
|
55
68
|
@initialized = true
|
|
56
69
|
end
|
|
57
70
|
end
|
|
@@ -72,13 +85,15 @@ module Foam
|
|
|
72
85
|
@mutex.synchronize { @providers.delete(kind) }
|
|
73
86
|
end
|
|
74
87
|
|
|
75
|
-
# Shutdown hands the providers back
|
|
76
|
-
# helpers go quiet again and state stops
|
|
88
|
+
# Shutdown hands the providers back, gives each slot Foam still holds back to the API, and
|
|
89
|
+
# leaves the state as it was before init, so the helpers go quiet again and state stops
|
|
90
|
+
# reporting slots we no longer hold.
|
|
77
91
|
def take_providers
|
|
78
92
|
@mutex.synchronize do
|
|
93
|
+
@claimed_slots.each { |signal, object| Slots.release(signal, object) }
|
|
79
94
|
providers = @providers.values.compact
|
|
80
95
|
@providers = {}
|
|
81
|
-
@
|
|
96
|
+
@claimed_slots = {}
|
|
82
97
|
@instrumentations = []
|
|
83
98
|
@instrumentation_config = {}
|
|
84
99
|
@late = []
|
|
@@ -104,10 +119,10 @@ module Foam
|
|
|
104
119
|
@mutex.synchronize { @providers[:logger] }
|
|
105
120
|
end
|
|
106
121
|
|
|
107
|
-
# The switch for body capture: set only by an init in advanced mode,
|
|
108
|
-
#
|
|
122
|
+
# The switch for body capture: set only by an init in advanced mode, and off while another
|
|
123
|
+
# SDK holds the logs slot, the rule the Node package follows too.
|
|
109
124
|
def body_redactor
|
|
110
|
-
@mutex.synchronize { @body_redactor }
|
|
125
|
+
@mutex.synchronize { @body_redactor unless %i[local none].include?(source(:logs)) }
|
|
111
126
|
end
|
|
112
127
|
|
|
113
128
|
# The network capture mode, :off until init runs and again after shutdown, so the Rack
|
|
@@ -116,6 +131,22 @@ module Foam
|
|
|
116
131
|
@mutex.synchronize { @network_capture }
|
|
117
132
|
end
|
|
118
133
|
|
|
134
|
+
# A forked child inherits the parent's providers. Their resource names the parent process,
|
|
135
|
+
# and the periodic metric reader's export thread did not survive the fork, so the child
|
|
136
|
+
# gets its own process attributes and a running reader. The batch processors reset on
|
|
137
|
+
# their own when the pid changes.
|
|
138
|
+
def after_fork
|
|
139
|
+
Safely.call("after fork") do
|
|
140
|
+
Resource.refresh(providers)
|
|
141
|
+
meter = @mutex.synchronize { @providers[:meter] }
|
|
142
|
+
next unless meter
|
|
143
|
+
|
|
144
|
+
meter.metric_readers.each do |reader|
|
|
145
|
+
reader.after_fork if reader.respond_to?(:after_fork)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
119
150
|
private
|
|
120
151
|
|
|
121
152
|
# A library loaded after init is never instrumented, and upstream says nothing about it at the
|
|
@@ -129,44 +160,28 @@ module Foam
|
|
|
129
160
|
end
|
|
130
161
|
end
|
|
131
162
|
|
|
132
|
-
# Anyone can overwrite the OpenTelemetry globals after init, so :global is checked against
|
|
133
|
-
# the live slot at call time rather than trusted from init.
|
|
134
163
|
def live_signals
|
|
135
|
-
@signals.to_h
|
|
136
|
-
# We only need to check signals marked as :global
|
|
137
|
-
next [signal, source] unless source == :global
|
|
138
|
-
|
|
139
|
-
# Make sure that the source is actually in the live slot
|
|
140
|
-
live = live_slot(signal)
|
|
141
|
-
next [signal, source] if live.equal?(installed(signal))
|
|
142
|
-
|
|
143
|
-
# Store overwritten slots in @taken so we only send a warning the first time
|
|
144
|
-
unless @taken.include?(signal)
|
|
145
|
-
@taken << signal
|
|
146
|
-
Diagnostics.warn("#{signal} global slot taken by #{live.class}")
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Foam's logger provider keeps working without the slot, so log still delivers.
|
|
150
|
-
[signal, (signal == :logs) ? :local : :none]
|
|
151
|
-
end
|
|
164
|
+
@signals.to_h { |signal, _| [signal, source(signal)] }
|
|
152
165
|
end
|
|
153
166
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
# Anyone can overwrite the OpenTelemetry globals after init, so a signal recorded as
|
|
168
|
+
# :global is checked against the live slot at call time, and warned about once when
|
|
169
|
+
# displaced.
|
|
170
|
+
def source(signal)
|
|
171
|
+
return @signals[signal] unless @signals[signal] == :global
|
|
172
|
+
return :global if Slots.live(signal).equal?(@claimed_slots[signal])
|
|
173
|
+
|
|
174
|
+
unless @taken.include?(signal)
|
|
175
|
+
@taken << signal
|
|
176
|
+
Diagnostics.warn("#{signal} global slot taken by #{Slots.live(signal).class}")
|
|
160
177
|
end
|
|
178
|
+
without_slot(signal)
|
|
161
179
|
end
|
|
162
180
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
when :logs then @providers[:logger]
|
|
168
|
-
when :baggage then @propagator
|
|
169
|
-
end
|
|
181
|
+
# What a signal reports without its slot. Foam's logger provider keeps working, so log
|
|
182
|
+
# still delivers; the other helpers go quiet.
|
|
183
|
+
def without_slot(signal)
|
|
184
|
+
(signal == :logs) ? :local : :none
|
|
170
185
|
end
|
|
171
186
|
end
|
|
172
187
|
end
|
|
@@ -12,8 +12,16 @@ module Foam
|
|
|
12
12
|
span = OpenTelemetry::Trace.current_span
|
|
13
13
|
return unless span.recording?
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if exception.is_a?(Exception)
|
|
16
|
+
span.record_exception(exception, attributes: attributes)
|
|
17
|
+
message = exception.message
|
|
18
|
+
else
|
|
19
|
+
# Anything else has no type or backtrace to record, so only its text lands on the event.
|
|
20
|
+
message = exception.to_s
|
|
21
|
+
event_attributes = {"exception.message" => message}.merge(attributes.to_h)
|
|
22
|
+
span.add_event("exception", attributes: event_attributes)
|
|
23
|
+
end
|
|
24
|
+
span.status = OpenTelemetry::Trace::Status.error(message)
|
|
17
25
|
nil
|
|
18
26
|
end
|
|
19
27
|
end
|
data/lib/foam/otel/version.rb
CHANGED
data/lib/foam/otel.rb
CHANGED
|
@@ -17,13 +17,17 @@ require_relative "otel/internal/body_capture"
|
|
|
17
17
|
require_relative "otel/internal/rack/input_tee"
|
|
18
18
|
require_relative "otel/internal/rack/body_tee"
|
|
19
19
|
require_relative "rack/middleware"
|
|
20
|
+
require_relative "otel/internal/clients/read_tee"
|
|
20
21
|
require_relative "otel/internal/clients/net_http"
|
|
21
22
|
require_relative "otel/internal/clients/excon"
|
|
22
23
|
require_relative "otel/internal/clients/http_client"
|
|
23
24
|
require_relative "otel/internal/clients/faraday"
|
|
25
|
+
require_relative "otel/internal/aws_lambda"
|
|
24
26
|
require_relative "otel/internal/before_send"
|
|
25
27
|
require_relative "otel/internal/instrumentations"
|
|
26
28
|
require_relative "otel/internal/ignored_hosts"
|
|
29
|
+
require_relative "otel/internal/slots"
|
|
30
|
+
require_relative "otel/internal/fork_hooks"
|
|
27
31
|
require_relative "otel/internal/resource"
|
|
28
32
|
require_relative "otel/internal/init"
|
|
29
33
|
require_relative "otel/internal/exporters"
|
|
@@ -186,13 +190,13 @@ module Foam
|
|
|
186
190
|
end
|
|
187
191
|
end
|
|
188
192
|
|
|
189
|
-
def flush(timeout:
|
|
193
|
+
def flush(timeout: Internal::Constants::LIFECYCLE_TIMEOUT)
|
|
190
194
|
Internal::Safely.call("flush") do
|
|
191
195
|
Internal::Lifecycle.flush(timeout: timeout)
|
|
192
196
|
end
|
|
193
197
|
end
|
|
194
198
|
|
|
195
|
-
def shutdown(timeout:
|
|
199
|
+
def shutdown(timeout: Internal::Constants::LIFECYCLE_TIMEOUT)
|
|
196
200
|
Internal::Safely.call("shutdown") do
|
|
197
201
|
Internal::Lifecycle.shutdown(timeout: timeout)
|
|
198
202
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foam-otel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.0.alpha.
|
|
4
|
+
version: 3.0.0.alpha.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foam
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: opentelemetry-exporter-otlp
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- lib/foam-otel.rb
|
|
161
161
|
- lib/foam/otel.rb
|
|
162
162
|
- lib/foam/otel/internal.rb
|
|
163
|
+
- lib/foam/otel/internal/aws_lambda.rb
|
|
163
164
|
- lib/foam/otel/internal/before_send.rb
|
|
164
165
|
- lib/foam/otel/internal/body_capture.rb
|
|
165
166
|
- lib/foam/otel/internal/body_decoder.rb
|
|
@@ -168,11 +169,13 @@ files:
|
|
|
168
169
|
- lib/foam/otel/internal/clients/faraday.rb
|
|
169
170
|
- lib/foam/otel/internal/clients/http_client.rb
|
|
170
171
|
- lib/foam/otel/internal/clients/net_http.rb
|
|
172
|
+
- lib/foam/otel/internal/clients/read_tee.rb
|
|
171
173
|
- lib/foam/otel/internal/constants.rb
|
|
172
174
|
- lib/foam/otel/internal/content_type.rb
|
|
173
175
|
- lib/foam/otel/internal/diagnostics.rb
|
|
174
176
|
- lib/foam/otel/internal/endpoint.rb
|
|
175
177
|
- lib/foam/otel/internal/exporters.rb
|
|
178
|
+
- lib/foam/otel/internal/fork_hooks.rb
|
|
176
179
|
- lib/foam/otel/internal/ignored_hosts.rb
|
|
177
180
|
- lib/foam/otel/internal/ingest.rb
|
|
178
181
|
- lib/foam/otel/internal/init.rb
|
|
@@ -190,6 +193,7 @@ files:
|
|
|
190
193
|
- lib/foam/otel/internal/report.rb
|
|
191
194
|
- lib/foam/otel/internal/resource.rb
|
|
192
195
|
- lib/foam/otel/internal/safely.rb
|
|
196
|
+
- lib/foam/otel/internal/slots.rb
|
|
193
197
|
- lib/foam/otel/internal/state.rb
|
|
194
198
|
- lib/foam/otel/internal/traces.rb
|
|
195
199
|
- lib/foam/otel/version.rb
|