foam-otel 3.0.0.alpha.11 → 3.0.0.alpha.13
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 +103 -37
- data/lib/foam/otel/internal/before_send.rb +15 -8
- data/lib/foam/otel/internal/body_capture.rb +25 -6
- data/lib/foam/otel/internal/body_decoder.rb +6 -6
- data/lib/foam/otel/internal/body_redactor.rb +3 -3
- data/lib/foam/otel/internal/clients/excon.rb +16 -28
- data/lib/foam/otel/internal/clients/faraday.rb +15 -21
- data/lib/foam/otel/internal/clients/http_client.rb +9 -15
- data/lib/foam/otel/internal/clients/net_http.rb +12 -23
- data/lib/foam/otel/internal/constants.rb +3 -0
- data/lib/foam/otel/internal/exporters.rb +42 -28
- data/lib/foam/otel/internal/fork_hooks.rb +4 -5
- data/lib/foam/otel/internal/ignored_headers.rb +1 -1
- data/lib/foam/otel/internal/ignored_hosts.rb +0 -1
- data/lib/foam/otel/internal/ingest.rb +7 -6
- data/lib/foam/otel/internal/init.rb +74 -72
- data/lib/foam/otel/internal/instrumentations.rb +9 -3
- data/lib/foam/otel/internal/lifecycle.rb +14 -4
- data/lib/foam/otel/internal/log_record_processor.rb +5 -4
- data/lib/foam/otel/internal/otlp.rb +1 -0
- data/lib/foam/otel/internal/patch_resolver.rb +6 -7
- data/lib/foam/otel/internal/propagation.rb +15 -25
- data/lib/foam/otel/internal/que.rb +6 -6
- data/lib/foam/otel/internal/rails_middleware.rb +12 -8
- data/lib/foam/otel/internal/redactor.rb +75 -31
- data/lib/foam/otel/internal/resource.rb +3 -5
- data/lib/foam/otel/internal/safely.rb +7 -4
- data/lib/foam/otel/internal/state.rb +26 -35
- data/lib/foam/otel/version.rb +1 -1
- data/lib/foam/rack/middleware.rb +31 -18
- metadata +18 -18
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '026729cab71cf3ea55acaf802d5efc5f6e2d71e6de6ae72f5ed57c1181f11dd2'
|
|
4
|
+
data.tar.gz: 1dc901d627cd9e326cfc5fa5f9339d53e2d9af3c9de3b2e14f64a9d4784dcd77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e037404d73d8dcc83dbadb700b5395c41097706e2b2772597c33a577fb5b5a806df0eb0546d09dc05202ab56e49e3df0f7b2ea1f55025156691d0d014feecbf6
|
|
7
|
+
data.tar.gz: 64bcbecd4e4c539bc4190ba35c9a7845778775857180190408371c75321fdd17434e5e6698d97ab9a69dc2f1c5859d569ba69d4b027ae9f3ffb0c7598916c32e
|
data/README.md
CHANGED
|
@@ -23,54 +23,62 @@ bundle install
|
|
|
23
23
|
|
|
24
24
|
## Initialize
|
|
25
25
|
|
|
26
|
-
Call `init` once, after the app's gems are loaded and before the app runs. Set `enabled
|
|
26
|
+
Call `init` once, after the app's gems are loaded and before the app runs. Set `enabled` to `true` only in the deployed environments that send telemetry to Foam, such as production.
|
|
27
27
|
|
|
28
28
|
Store the ingest token as `FOAM_OTEL_TOKEN` and pass `ENV["FOAM_OTEL_TOKEN"]` to `init`.
|
|
29
29
|
|
|
30
30
|
### Rails
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Call `init` after every file in `config/initializers` and before Rails builds the middleware stack. Add an initializer to the application class in `config/application.rb`:
|
|
33
33
|
|
|
34
34
|
```ruby
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
module CheckoutApi
|
|
36
|
+
class Application < Rails::Application
|
|
37
|
+
initializer "foam.init", after: :load_config_initializers do
|
|
38
|
+
Foam.init(
|
|
39
|
+
name: "checkout-api",
|
|
40
|
+
environment: Rails.env,
|
|
41
|
+
enabled: Rails.env.production?,
|
|
42
|
+
token: ENV["FOAM_OTEL_TOKEN"]
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
`init`
|
|
49
|
+
`init` inserts `Foam::Rack::Middleware` directly after the OpenTelemetry Rack middleware. `config.ru` stays as generated. An `init` from `after_initialize` or later warns on stderr and adds no middleware.
|
|
44
50
|
|
|
45
51
|
Do not load Foam with a `RUBYOPT` preload. It runs before Rails loads, so it installs no Rails instrumentation and adds no middleware.
|
|
46
52
|
|
|
47
53
|
### Plain Ruby and Rack
|
|
48
54
|
|
|
49
|
-
Call `init`
|
|
55
|
+
Call `init` in `config.ru` after the app's requires, then add Foam's middleware:
|
|
50
56
|
|
|
51
57
|
```ruby
|
|
52
58
|
require "foam/otel"
|
|
59
|
+
require_relative "app"
|
|
53
60
|
|
|
54
61
|
Foam.init(
|
|
55
62
|
name: "checkout-api",
|
|
56
63
|
environment: ENV["RACK_ENV"],
|
|
57
|
-
enabled:
|
|
64
|
+
enabled: ENV["RACK_ENV"] == "production",
|
|
58
65
|
token: ENV["FOAM_OTEL_TOKEN"]
|
|
59
66
|
)
|
|
60
67
|
|
|
61
|
-
use(*OpenTelemetry::Instrumentation::Rack::Instrumentation.instance.middleware_args)
|
|
62
68
|
use Foam::Rack::Middleware
|
|
63
69
|
|
|
64
70
|
run App
|
|
65
71
|
```
|
|
66
72
|
|
|
73
|
+
`Foam::Rack::Middleware` runs the OpenTelemetry Rack middleware, which opens the server span. When `init` is disabled or stops early, the middleware passes every request through.
|
|
74
|
+
|
|
67
75
|
Foam's middleware records the `content-type`, `content-length`, and `content-encoding` headers as server span attributes. In `:advanced` mode it also sends the bodies as log records correlated to the span. See Network capture.
|
|
68
76
|
|
|
69
77
|
## Instrumentation
|
|
70
78
|
|
|
71
79
|
`init` installs OpenTelemetry's Ruby instrumentation bundle, `opentelemetry-instrumentation-all`, and the standard library logger bridge, `opentelemetry-instrumentation-logger`. An instrumentation installs only when its library is loaded before `init`, so call `init` after `Bundler.require` or after the app's own requires. A library loaded after `init` is not instrumented, and a warning is written to stderr once when `state` finds one. `state[:instrumentations]` lists what installed.
|
|
72
80
|
|
|
73
|
-
The bundle covers: action_mailer, action_pack, action_view, active_job, active_model_serializers, active_record, active_storage, active_support, anthropic, aws_lambda, aws_sdk, bunny, concurrent_ruby, dalli, delayed_job, ethon, excon, faraday, grape, graphql, grpc, gruf, http, http_client, httpx, koala, lmdb, mongo, mysql2, net_http, pg, que, racecar, rack, rails, rake, rdkafka, redis, resque, restclient, ruby_kafka, sidekiq, sinatra, trilogy.
|
|
81
|
+
The bundle covers: action_mailer, action_pack, action_view, active_job, active_model_serializers, active_record, active_storage, active_support, anthropic, aws_lambda, aws_sdk, bunny, concurrent_ruby, dalli, delayed_job, ethon, excon, faraday, grape, graphql, grpc, gruf, http, http_client, httpx, koala, lmdb, mongo, mysql2, net_http, openai, pg, que, racecar, rack, rails, rake, rdkafka, redis, resque, restclient, ruby_kafka, sidekiq, sinatra, trilogy.
|
|
74
82
|
|
|
75
83
|
To install an instrumentation outside the bundle, list it in `additional_instrumentations`.
|
|
76
84
|
|
|
@@ -88,11 +96,19 @@ To disable one instrumentation or change its options, pass it in `additional_ins
|
|
|
88
96
|
|
|
89
97
|
TODO(jhartquist): `db_capture` init option. The `pg`, `mysql2`, and `trilogy` instrumentations can include the statement (`db_statement: :include`) and `pg` can report row counts (`db_response_returned_rows`, `db_response_affected_rows`), but none hands over results. Result capture needs Foam patches on the drivers, opt-in, through the same redaction and size caps as bodies. See Expanded data capture at the end of this document.
|
|
90
98
|
|
|
91
|
-
TODO(jhartquist): Gen AI providers beyond `anthropic`. There is no OpenTelemetry Ruby instrumentation for the `
|
|
99
|
+
TODO(jhartquist): Gen AI providers beyond `anthropic` and `openai`. There is no OpenTelemetry Ruby instrumentation for the `ruby-openai` gem.
|
|
92
100
|
|
|
93
101
|
## Public API
|
|
94
102
|
|
|
95
|
-
Every method lives on the `Foam` module. A
|
|
103
|
+
Every method lives on the `Foam` module. A call with wrong arguments raises `ArgumentError`: an unknown keyword, a missing required keyword, or a wrong argument count. Any other problem, such as a blank or invalid value, is written to stderr with a `[foam-otel]` prefix, and the method returns without raising.
|
|
104
|
+
|
|
105
|
+
These helpers do nothing until Foam has a way to export:
|
|
106
|
+
|
|
107
|
+
- `set_baggage` works after `init`.
|
|
108
|
+
- The metric helpers work after `init`, or with `create_foam_ingest_metric_reader` attached.
|
|
109
|
+
- `log` works after `init`, or once `create_foam_ingest_log_record_processor` is built.
|
|
110
|
+
|
|
111
|
+
The trace context helpers and `record_exception` work on the current OpenTelemetry context, with or without `init`.
|
|
96
112
|
|
|
97
113
|
### `init`
|
|
98
114
|
|
|
@@ -140,7 +156,7 @@ Foam.init(
|
|
|
140
156
|
|
|
141
157
|
- `name` - the service name, sent as `service.name`.
|
|
142
158
|
- `environment` - the deployment environment, sent as `deployment.environment.name`.
|
|
143
|
-
- `enabled` - `false` turns Foam off; `init` returns without configuring OpenTelemetry. With a token, it still reports the outcome (see below).
|
|
159
|
+
- `enabled` - `false` turns Foam off; `init` returns without configuring OpenTelemetry. With a token, it still reports the outcome (see below). `OTEL_SDK_DISABLED=true` also turns Foam off.
|
|
144
160
|
- `token` - the Foam ingest token, sent as a bearer token on every export. Required when Foam is enabled; a missing token is reported on stderr and leaves Foam uninitialized.
|
|
145
161
|
- `version` - the service version, sent as `service.version` when given.
|
|
146
162
|
- `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.
|
|
@@ -164,11 +180,11 @@ Every signal carries a resource with these attributes:
|
|
|
164
180
|
- `host.name`, `host.arch`, `host.id` when it can be read, `os.type`, and `os.version`.
|
|
165
181
|
- The process and SDK attributes the OpenTelemetry SDK adds.
|
|
166
182
|
|
|
167
|
-
A process forked after `init` carries its own `service.instance.id` and `process.pid`. `OTEL_RESOURCE_ATTRIBUTES` can add further attributes. It cannot override
|
|
183
|
+
A process forked after `init` carries its own `service.instance.id` and `process.pid`. `OTEL_RESOURCE_ATTRIBUTES` can add further attributes and override the process and SDK attributes the OpenTelemetry SDK adds, except `process.pid`. It cannot override the other attributes above, and `OTEL_SERVICE_NAME` cannot override `name`.
|
|
168
184
|
|
|
169
|
-
`init` raises
|
|
185
|
+
`init` raises `ArgumentError` only when the `name` or `environment` keyword is left out or a keyword is unknown. A blank value, an invalid option value, or any other failure is written to stderr with a `[foam-otel]` prefix and leaves `state` uninitialized.
|
|
170
186
|
|
|
171
|
-
`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
|
|
187
|
+
`init` also reports its outcome to Foam: one log record with the message and the `state`, sent straight to the endpoint with the token. `init` resolves the endpoint's host before it returns, which takes up to 1 second when DNS is slow. The report itself is sent in the background. 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.
|
|
172
188
|
|
|
173
189
|
### `set_endpoint(url)`
|
|
174
190
|
|
|
@@ -219,15 +235,20 @@ meter_provider.add_metric_reader(reader)
|
|
|
219
235
|
|
|
220
236
|
All three export to the endpoint set by `set_endpoint` and take the same `redact` option as `init`. The span processor and the log record processor also take the same `before_send` option; the metric reader does not. Each one records its signal as `ingest` in `state` and reports the attachment to Foam.
|
|
221
237
|
|
|
222
|
-
Foam identifies the service by the `name` and `environment` passed in
|
|
238
|
+
Foam identifies the service by the `name` and `environment` passed in. On the copy sent to Foam, they overwrite the provider's `service.name` and `deployment.environment.name`. The copy also gets `foam.ingest.host`, the host of the endpoint it goes to. The provider's own telemetry stays unchanged. Pass the same values to all three.
|
|
223
239
|
|
|
224
|
-
|
|
240
|
+
Only that helper turns off when it gets one of these values:
|
|
241
|
+
|
|
242
|
+
- a missing or blank `name`, `environment`, or `token`
|
|
243
|
+
- an invalid `redact` or `before_send` value
|
|
244
|
+
|
|
245
|
+
It writes the problem to stderr and returns a component that exports nothing. The app adds that component to its provider as usual, and the signal stays `none` in `state`. An unset environment variable never stops the app from booting.
|
|
225
246
|
|
|
226
247
|
Do not combine `init` and an ingest component for the same signal. The ingest components do not install instrumentations; register the ones the app needs with the OpenTelemetry SDK directly.
|
|
227
248
|
|
|
228
249
|
### `inject_trace_context(carrier)`
|
|
229
250
|
|
|
230
|
-
Writes the W3C `traceparent`, `tracestate`, and `baggage` headers for the current span into `carrier`, a hash of string keys. A `tracestate` the carrier already holds is kept, since Foam adds no entry of its own and another SDK may have added one.
|
|
251
|
+
Writes the W3C `traceparent`, `tracestate`, and `baggage` headers for the current span into `carrier`, a hash of string keys. A `tracestate` the carrier already holds is kept, since Foam adds no entry of its own and another SDK may have added one. A `traceparent` the carrier already holds is overwritten.
|
|
231
252
|
|
|
232
253
|
Use it when the app sends a message itself (a Kafka record, a queue payload, a custom socket). The HTTP instrumentations already inject these headers; do not call it on ordinary HTTP requests. Without an active span it writes only `baggage`, and only when baggage is set.
|
|
233
254
|
|
|
@@ -237,8 +258,6 @@ Foam.inject_trace_context(headers)
|
|
|
237
258
|
producer.publish(topic, payload, headers: headers)
|
|
238
259
|
```
|
|
239
260
|
|
|
240
|
-
TODO(jhartquist): a carrier that already holds a `traceparent` has it overwritten. Decide whether to keep the existing one, replace it, or link the two.
|
|
241
|
-
|
|
242
261
|
### `extract_trace_context(carrier)`
|
|
243
262
|
|
|
244
263
|
Reads those headers from `carrier` and returns an OpenTelemetry context. Run the receiving work inside it so its spans continue the sender's trace. Returns the current context when the carrier has no trace headers.
|
|
@@ -335,11 +354,17 @@ Foam.flush(timeout: 5)
|
|
|
335
354
|
|
|
336
355
|
### `shutdown(timeout: 10)`
|
|
337
356
|
|
|
338
|
-
Flushes and shuts down Foam's providers,
|
|
357
|
+
Flushes and shuts down Foam's providers and the ingest components, and releases the global slots Foam claimed. The providers include the one behind `log` when an ingest component created it. The first `init` or ingest factory call registers it with `at_exit`, once per process. Most apps never call it. An app that never shuts its own providers down still delivers the ingest components' last batch. An app calls it before a process is killed without a normal exit.
|
|
339
358
|
|
|
340
359
|
`timeout` is in seconds and bounds the whole call. A provider that runs out of time is reported on stderr.
|
|
341
360
|
|
|
342
|
-
`at_exit` covers a normal exit, an unhandled exception, and a signal the process handles by exiting. The last batch is lost
|
|
361
|
+
`at_exit` covers a normal exit, an unhandled exception, and a signal the process handles by exiting. The last batch is lost only in these cases:
|
|
362
|
+
|
|
363
|
+
- The process ends without running `at_exit`: `exit!`, a `KILL` signal, or a crash of the Ruby process itself.
|
|
364
|
+
- A batch outlasts the timeout.
|
|
365
|
+
- The export fails, for example when the endpoint is unreachable.
|
|
366
|
+
|
|
367
|
+
An exception outside any span is invisible unless the app logs it.
|
|
343
368
|
|
|
344
369
|
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.
|
|
345
370
|
|
|
@@ -365,7 +390,7 @@ Foam.state
|
|
|
365
390
|
# }
|
|
366
391
|
```
|
|
367
392
|
|
|
368
|
-
`instrumentations` lists
|
|
393
|
+
`instrumentations` lists each instrumentation that installed, by gem name without the `opentelemetry-instrumentation-` prefix. Each entry carries the version of the library it instruments, as `name@version`. `net_http` reports the Ruby version, and `aws_lambda` has no version. An instrumentation whose library is absent or unsupported is left out.
|
|
369
394
|
|
|
370
395
|
A library loaded after `init` is not instrumented and stays out of the list; a warning is written to stderr the first time a `state` call finds one. See Instrumentation for the bundle.
|
|
371
396
|
|
|
@@ -390,15 +415,28 @@ TODO(jhartquist): custom spans and the OpenTelemetry API: getting a tracer, `in_
|
|
|
390
415
|
|
|
391
416
|
- `:off` keeps the standard OpenTelemetry HTTP telemetry, with no extra capture.
|
|
392
417
|
- `:basic` adds the `content-type`, `content-length`, and `content-encoding` headers of the request and the response as `http.request.header.<name>` and `http.response.header.<name>` span attributes. No other header attribute is added.
|
|
393
|
-
- `:advanced` adds the request and response bodies as well.
|
|
418
|
+
- `:advanced` adds the request and response bodies as well, and turns on the `openai` instrumentation's `capture_content`: prompts and completions arrive as `gen_ai` log records, redacted like bodies. `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` takes precedence. The `anthropic` instrumentation has no content capture; its prompts arrive only as captured `Net::HTTP` request bodies.
|
|
394
419
|
|
|
395
420
|
There is no option to record other headers.
|
|
396
421
|
|
|
397
|
-
Server spans get the capture from `Foam::Rack::Middleware`,
|
|
422
|
+
Server spans get the capture from `Foam::Rack::Middleware`, which runs inside the OpenTelemetry Rack middleware (added by `init` in Rails apps). Client spans get it from `Net::HTTP`, Faraday with any adapter, Excon, and HTTPClient when their gem is loaded before `init`. RestClient and HTTParty are captured on the `Net::HTTP` span beneath them. `http`, `httpx`, and `ethon` get no capture, and neither does a Faraday connection built before `init`.
|
|
423
|
+
|
|
424
|
+
Foam captures these streamed bodies:
|
|
425
|
+
|
|
426
|
+
- a Rack response body the server sends through `each`
|
|
427
|
+
- a Rack request body, including the part the app leaves unread when `rack.input` can seek
|
|
428
|
+
- a `Net::HTTP` request body sent through `body_stream`
|
|
429
|
+
- an HTTPClient response read in a block
|
|
398
430
|
|
|
399
|
-
|
|
431
|
+
Foam captures no other streamed body. For example:
|
|
400
432
|
|
|
401
|
-
|
|
433
|
+
- a Rack response body that responds to `call`
|
|
434
|
+
- a file the server sends by path (`to_path`)
|
|
435
|
+
- a hijacked connection (`rack.hijack`)
|
|
436
|
+
- an Excon, Faraday, or HTTPClient request body sent from an IO
|
|
437
|
+
- an Excon `request_block` or an HTTPClient multipart body
|
|
438
|
+
- a `Net::HTTP` response read in a block or into an IO
|
|
439
|
+
- an Excon response read through `response_block` or a Faraday response read through `on_data`
|
|
402
440
|
|
|
403
441
|
### Bodies
|
|
404
442
|
|
|
@@ -407,7 +445,7 @@ TODO(jhartquist): cover `http`, `httpx`, `ethon`, and `restclient`.
|
|
|
407
445
|
- At most 1 MiB of a body is kept, sent as records of at most 64 KiB each. The rest counts toward the size.
|
|
408
446
|
- Bodies are parsed by content type and redacted with the same keys as span attributes. See Redaction.
|
|
409
447
|
- A server request body is captured whether or not the app reads it when the input is rewindable, as it is under Puma. Otherwise only what the app reads is captured.
|
|
410
|
-
- A streamed server response is observed as it is sent, without buffering or delaying it. Its records are emitted when the body closes
|
|
448
|
+
- A streamed server response is observed as it is sent, without buffering or delaying it. Its records are emitted when the body closes. A stream that stalls for over 60 seconds can be recorded as incomplete.
|
|
411
449
|
|
|
412
450
|
### Known issue
|
|
413
451
|
|
|
@@ -437,13 +475,39 @@ Secrets in a log line are masked before export. See Redaction.
|
|
|
437
475
|
|
|
438
476
|
## AWS Lambda
|
|
439
477
|
|
|
478
|
+
Wrap the function's handler with the `aws_lambda` instrumentation's handler, and call `init` in the wrapper file after the handler file loads.
|
|
479
|
+
|
|
480
|
+
1. Add `foam_lambda.rb` next to the handler file:
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
require "foam/otel"
|
|
484
|
+
require_relative "function"
|
|
485
|
+
|
|
486
|
+
Foam.init(name: "checkout-fn", environment: "production", token: ENV["FOAM_OTEL_TOKEN"])
|
|
487
|
+
|
|
488
|
+
FOAM_HANDLER = OpenTelemetry::Instrumentation::AwsLambda::Handler.new
|
|
489
|
+
|
|
490
|
+
def foam_handler(event:, context:)
|
|
491
|
+
FOAM_HANDLER.call_wrapped(event: event, context: context)
|
|
492
|
+
end
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
2. Set the function's handler to `foam_lambda.foam_handler`.
|
|
496
|
+
3. Set the `ORIG_HANDLER` environment variable to the original handler, such as `function.handler`.
|
|
497
|
+
|
|
440
498
|
The invocation span carries `faas.trigger` for API Gateway, ALB, SNS, SQS, S3, DynamoDB streams, and EventBridge schedule events.
|
|
441
499
|
|
|
442
|
-
Traces, metrics, and logs are flushed before the handler returns
|
|
500
|
+
Traces, metrics, and logs are flushed before the handler returns, including failing invocations.
|
|
443
501
|
|
|
444
502
|
## Pre-forking servers and threads
|
|
445
503
|
|
|
446
|
-
|
|
504
|
+
Call `init` once, in the process that forks:
|
|
505
|
+
|
|
506
|
+
- the master of Puma in cluster mode with `preload_app!` (the default since Puma 7)
|
|
507
|
+
- the master of Unicorn with `preload_app true`
|
|
508
|
+
- a worker that forks a child per job
|
|
509
|
+
|
|
510
|
+
Each child exports its own traces, logs, and metrics, with its own `process.pid`.
|
|
447
511
|
|
|
448
512
|
The current span and baggage are stored per fiber. A `Thread.new` or `Fiber.new` starts with none, so spans started inside it begin a new trace. To continue the trace, capture the context first and make it current inside the thread:
|
|
449
513
|
|
|
@@ -501,11 +565,11 @@ Foam.init(
|
|
|
501
565
|
Sentry sends its events to the DSN's host over HTTP, which Foam would otherwise record as spans. Pass that address and Sentry's request header to `init`. This works whether Sentry sends inline or from its background worker:
|
|
502
566
|
|
|
503
567
|
```ruby
|
|
504
|
-
dsn = Sentry.configuration
|
|
568
|
+
dsn = Sentry.configuration&.dsn
|
|
505
569
|
|
|
506
570
|
Foam.init(
|
|
507
571
|
name: "checkout-api", environment: "production", token: ENV["FOAM_OTEL_TOKEN"],
|
|
508
|
-
ignored_outbound_hosts: ["#{dsn.host}:#{dsn.port}"],
|
|
572
|
+
ignored_outbound_hosts: dsn ? ["#{dsn.host}:#{dsn.port}"] : [],
|
|
509
573
|
ignored_outbound_headers: ["X-Sentry-Auth"]
|
|
510
574
|
)
|
|
511
575
|
```
|
|
@@ -521,7 +585,7 @@ OpenTelemetry.propagation = OpenTelemetry::Context::Propagation::CompositeTextMa
|
|
|
521
585
|
|
|
522
586
|
### New Relic
|
|
523
587
|
|
|
524
|
-
New Relic's agent sends its data to its collector over HTTP,
|
|
588
|
+
New Relic's agent sends its data to its collector over HTTP. When it connects, it also probes the AWS, Azure, and GCP metadata endpoints. Foam records each of these requests as a span unless its host is ignored. The agent's requests carry no header of their own, so there is no header to ignore. Pass the collector's address and the metadata hosts to `init`:
|
|
525
589
|
|
|
526
590
|
```ruby
|
|
527
591
|
collector = "#{NewRelic::Agent.config[:host]}:#{NewRelic::Agent.config[:port]}"
|
|
@@ -536,6 +600,8 @@ Foam.init(
|
|
|
536
600
|
)
|
|
537
601
|
```
|
|
538
602
|
|
|
603
|
+
TODO(jhartquist): confirm the host New Relic's agent sends its data to. The agent sends `preconnect` to `Agent.config[:host]` and every later request to the `redirect_host` in the reply, while the list above names the configured host only. New Relic's network docs list exact collector hosts per region, so the redirect likely names the same host, but mosh's receiver never redirects. Check with a real license key and a debug agent log, in the US and EU regions.
|
|
604
|
+
|
|
539
605
|
Start New Relic before `init`, as Rails does through the agent's Railtie. If the agent instruments Net::HTTP in chain mode and starts after `init`, every Net::HTTP request raises `SystemStackError`. The agent uses chain mode when `instrumentation.net_http` is `chain`, or on its own next to ScoutApm, Rack::MiniProfiler, or Airbrake before 10.0.2. Where the order cannot change, set `instrumentation.net_http` to `prepend`.
|
|
540
606
|
|
|
541
607
|
If the app turns on New Relic's OpenTelemetry bridge (`opentelemetry.enabled`), New Relic takes the tracer provider and propagator, and Foam records no traces. Logs and metrics still reach Foam.
|
|
@@ -19,10 +19,10 @@ module Foam
|
|
|
19
19
|
freeze
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# the exporter
|
|
22
|
+
# Returns what to send for a hook event the exporters built, or nil to drop it. The event goes
|
|
23
|
+
# out unchanged when the hook raises, returns something other than an event of the same type,
|
|
24
|
+
# or gets an event that cannot be copied. A broken hook never loses telemetry. The block
|
|
25
|
+
# receives what the hook raised, and the exporter reports it once for its batch.
|
|
26
26
|
def event_to_export(hook_event)
|
|
27
27
|
return hook_event unless @hook
|
|
28
28
|
|
|
@@ -59,17 +59,24 @@ module Foam
|
|
|
59
59
|
result[:links].is_a?(Array) && result[:links].all? { |entry| span_link_entry?(entry) }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Attributes that are not a Hash, or a timestamp that is not an Integer, would fail the export
|
|
63
|
+
# of the whole batch.
|
|
62
64
|
def span_event_entry?(entry)
|
|
63
|
-
entry.is_a?(Hash) && entry[:name].is_a?(String)
|
|
65
|
+
entry.is_a?(Hash) && entry[:name].is_a?(String) &&
|
|
66
|
+
optional?(entry[:attributes], Hash) && optional?(entry[:timestamp], Integer)
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def span_link_entry?(entry)
|
|
67
|
-
entry.is_a?(Hash) && entry[:span_context].is_a?(OpenTelemetry::Trace::SpanContext)
|
|
70
|
+
entry.is_a?(Hash) && entry[:span_context].is_a?(OpenTelemetry::Trace::SpanContext) &&
|
|
71
|
+
optional?(entry[:attributes], Hash)
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def log_fields?(result)
|
|
71
|
-
(result[:severity_text]
|
|
72
|
-
|
|
75
|
+
optional?(result[:severity_text], String) && optional?(result[:severity_number], Integer)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def optional?(value, type)
|
|
79
|
+
value.nil? || value.is_a?(type)
|
|
73
80
|
end
|
|
74
81
|
|
|
75
82
|
NONE = new(nil)
|
|
@@ -78,8 +78,9 @@ module Foam
|
|
|
78
78
|
# - Foam has no logger provider
|
|
79
79
|
# - the limit is reached
|
|
80
80
|
# The block returns the headers as lowercase names to values; it runs at finalize, so
|
|
81
|
-
# headers set after the body started still count.
|
|
82
|
-
|
|
81
|
+
# headers set after the body started still count. body_redactor defaults to the one init
|
|
82
|
+
# built.
|
|
83
|
+
def start(span, side:, direction:, body_redactor: State.body_redactor, &headers)
|
|
83
84
|
Safely.call("#{direction} body capture") do
|
|
84
85
|
return unless body_redactor
|
|
85
86
|
return unless span.recording? && span.context.trace_flags.sampled?
|
|
@@ -97,6 +98,14 @@ module Foam
|
|
|
97
98
|
end
|
|
98
99
|
end
|
|
99
100
|
|
|
101
|
+
# Starts the request capture of a client span and gives it the whole body. Returns the
|
|
102
|
+
# capture, or nil when start captures nothing.
|
|
103
|
+
def client_request(span, body, &headers)
|
|
104
|
+
capture = start(span, side: :client, direction: :request, &headers)
|
|
105
|
+
capture&.observe(body)
|
|
106
|
+
capture
|
|
107
|
+
end
|
|
108
|
+
|
|
100
109
|
# The clock the deadline is measured on, in monotonic seconds.
|
|
101
110
|
def now
|
|
102
111
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
@@ -123,13 +132,23 @@ module Foam
|
|
|
123
132
|
# - the current span is not a recording client span, as for a sampled-out request or a
|
|
124
133
|
# host the instrumentation leaves untraced
|
|
125
134
|
# - a middleware above the transport owns the span and captures on it itself
|
|
135
|
+
# - the lookup failed
|
|
126
136
|
def client_span
|
|
127
|
-
|
|
137
|
+
Safely.call("body capture") do
|
|
138
|
+
return if OwnRequest.current?
|
|
139
|
+
|
|
140
|
+
span = OpenTelemetry::Trace.current_span
|
|
141
|
+
return unless span.recording? && span.kind == :client
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
span unless OpenTelemetry::Context.current.value(OWNER)&.span.equal?(span)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
131
146
|
|
|
132
|
-
|
|
147
|
+
# Returns the safe headers that are present, as lowercase names to values. The block finds
|
|
148
|
+
# the value of each name. Without a block, the lookup in hash ignores the case of names.
|
|
149
|
+
def safe_headers(hash = nil, &lookup)
|
|
150
|
+
lookup ||= ->(name) { hash[name] || hash.find { |key, _value| key.casecmp?(name) }&.last }
|
|
151
|
+
Constants::SAFE_NETWORK_HEADERS.to_h { |name| [name, lookup.call(name)] }.compact
|
|
133
152
|
end
|
|
134
153
|
|
|
135
154
|
# Sets the safe headers of one direction on the span, in the semantic conventions' dash
|
|
@@ -9,8 +9,6 @@ module Foam
|
|
|
9
9
|
# encoding within a bound, reads the charset, cuts the text at the capture limit, and splits
|
|
10
10
|
# it into chunks. A body that is not text yields no text, and only its metadata is recorded.
|
|
11
11
|
module BodyDecoder
|
|
12
|
-
INFLATED_ENCODINGS = %w[gzip x-gzip deflate].freeze
|
|
13
|
-
|
|
14
12
|
# How much of a body of unknown type is looked at to tell whether it is text.
|
|
15
13
|
SNIFF_BYTES = 1024
|
|
16
14
|
# Control characters other than tab, line feed, form feed, and carriage return.
|
|
@@ -33,9 +31,11 @@ module Foam
|
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
# Returns the body as UTF-8 text and whether it was cut at the capture limit. Invalid
|
|
36
|
-
# bytes are replaced and the cut lands on a character boundary. Returns nil when
|
|
37
|
-
# content type is binary
|
|
38
|
-
#
|
|
34
|
+
# bytes are replaced, and the cut lands on a character boundary. Returns nil when:
|
|
35
|
+
# - the content type is binary
|
|
36
|
+
# - the content type is unknown or missing, and the bytes do not read as text
|
|
37
|
+
# - the bytes are a file, whatever the type says
|
|
38
|
+
# A multipart body is never judged a file as a whole; its parts are judged on their own.
|
|
39
39
|
def text(bytes, content_type)
|
|
40
40
|
readable = content_type.text? || (!content_type.binary? && textual?(bytes))
|
|
41
41
|
readable &&= content_type.multipart? || !file?(bytes)
|
|
@@ -84,7 +84,7 @@ module Foam
|
|
|
84
84
|
# framing alike, and its block form hands over the output in pieces, so inflating stops
|
|
85
85
|
# as soon as the bound is passed.
|
|
86
86
|
def inflate(data, encoding)
|
|
87
|
-
return unless INFLATED_ENCODINGS.include?(encoding)
|
|
87
|
+
return unless Constants::INFLATED_ENCODINGS.include?(encoding)
|
|
88
88
|
|
|
89
89
|
inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
|
|
90
90
|
output = +""
|
|
@@ -48,9 +48,9 @@ module Foam
|
|
|
48
48
|
freeze
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Masks the values under sensitive keys
|
|
52
|
-
# missing type is read as plain text, a binary type is left alone
|
|
53
|
-
# whole body
|
|
51
|
+
# Masks the values under sensitive keys, reading the body by its content type. A body of
|
|
52
|
+
# unknown or missing type is read as plain text, and a binary type is left alone. A failure
|
|
53
|
+
# masks the whole body, so nothing leaks.
|
|
54
54
|
def redact(text, content_type)
|
|
55
55
|
type = ContentType.parse(content_type)
|
|
56
56
|
redacted =
|
|
@@ -13,9 +13,6 @@ module Foam
|
|
|
13
13
|
# response or the error.
|
|
14
14
|
REQUEST_CAPTURE = :foam_request_capture
|
|
15
15
|
|
|
16
|
-
# The encodings Excon's Decompress middleware inflates.
|
|
17
|
-
DECOMPRESSED = %w[gzip x-gzip deflate].freeze
|
|
18
|
-
|
|
19
16
|
def self.install
|
|
20
17
|
return unless Instrumentations.installed?(INSTRUMENTATION)
|
|
21
18
|
|
|
@@ -33,7 +30,7 @@ module Foam
|
|
|
33
30
|
# Captures a String request body before it is written and keeps the capture on the
|
|
34
31
|
# datum until the response or the error arrives.
|
|
35
32
|
def request_call(datum)
|
|
36
|
-
span =
|
|
33
|
+
span = BodyCapture.client_span
|
|
37
34
|
if span
|
|
38
35
|
datum[REQUEST_CAPTURE] =
|
|
39
36
|
Safely.call("request body capture") { capture_request(datum, span) }
|
|
@@ -46,15 +43,18 @@ module Foam
|
|
|
46
43
|
# headers of both directions, now that Excon has written the request's.
|
|
47
44
|
def response_call(datum)
|
|
48
45
|
datum.delete(REQUEST_CAPTURE)&.finalize(complete: true)
|
|
49
|
-
|
|
46
|
+
span = BodyCapture.client_span
|
|
47
|
+
if span
|
|
50
48
|
Safely.call("request header capture") do
|
|
51
|
-
BodyCapture.
|
|
49
|
+
headers = BodyCapture.safe_headers(datum[:headers])
|
|
50
|
+
BodyCapture.set_header_attributes(span, :request, headers)
|
|
52
51
|
end
|
|
53
52
|
Safely.call("response header capture") do
|
|
54
|
-
BodyCapture.
|
|
53
|
+
headers = BodyCapture.safe_headers(datum[:response][:headers])
|
|
54
|
+
BodyCapture.set_header_attributes(span, :response, headers)
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
|
-
Safely.call("response body capture") { capture_response(datum) }
|
|
57
|
+
Safely.call("response body capture") { capture_response(datum, span) }
|
|
58
58
|
@stack.response_call(datum)
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -70,25 +70,20 @@ module Foam
|
|
|
70
70
|
body = datum[:body]
|
|
71
71
|
return unless body.is_a?(String) && !datum.key?(:request_block)
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
span, State.body_redactor, side: :client, direction: :request
|
|
75
|
-
) { headers(datum[:headers]) }
|
|
76
|
-
capture&.observe(body)
|
|
77
|
-
capture
|
|
73
|
+
BodyCapture.client_request(span, body) { BodyCapture.safe_headers(datum[:headers]) }
|
|
78
74
|
end
|
|
79
75
|
|
|
80
76
|
# Captures the parsed response body. A response handed to a response block never reaches
|
|
81
77
|
# the datum, so it is skipped. When a middleware above owns the request, it is told
|
|
82
78
|
# instead what Decompress is about to inflate.
|
|
83
|
-
def capture_response(datum)
|
|
84
|
-
span = BodyCapture.client_span
|
|
79
|
+
def capture_response(datum, span)
|
|
85
80
|
body = datum.dig(:response, :body)
|
|
86
81
|
return unless body.is_a?(String) && !datum.key?(:response_block)
|
|
87
82
|
return report_inflating(datum, body) unless span
|
|
88
83
|
|
|
89
|
-
capture = BodyCapture.start(
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
capture = BodyCapture.start(span, side: :client, direction: :response) do
|
|
85
|
+
BodyCapture.safe_headers(datum[:response][:headers])
|
|
86
|
+
end
|
|
92
87
|
return unless capture
|
|
93
88
|
|
|
94
89
|
capture.observe(body)
|
|
@@ -101,19 +96,12 @@ module Foam
|
|
|
101
96
|
def report_inflating(datum, body)
|
|
102
97
|
return unless datum[:middlewares].include?(::Excon::Middleware::Decompress)
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
|
|
99
|
+
header = BodyCapture.safe_headers(datum[:response][:headers])["content-encoding"]
|
|
100
|
+
encoding = header&.split(",")&.last&.strip
|
|
101
|
+
return unless Constants::INFLATED_ENCODINGS.include?(encoding&.downcase)
|
|
106
102
|
|
|
107
103
|
BodyCapture.inflated_below(encoding, body.bytesize)
|
|
108
104
|
end
|
|
109
|
-
|
|
110
|
-
# Matched without regard to case, since the request headers keep whatever case the app
|
|
111
|
-
# wrote.
|
|
112
|
-
def headers(hash)
|
|
113
|
-
Constants::SAFE_NETWORK_HEADERS.to_h do |name|
|
|
114
|
-
[name, hash.find { |key, _value| key.casecmp?(name) }&.last]
|
|
115
|
-
end.compact
|
|
116
|
-
end
|
|
117
105
|
end
|
|
118
106
|
end
|
|
119
107
|
end
|