feat(observe): export settings from the standard OpenTelemetry variables, and an event time on log records - #520
Merged
Merged
Conversation
Start the telemetry setup in a child process pointed at a local OTLP/HTTP listener, and record the startup output and every exported span, log record and metric as a baseline. The baseline is recorded before any change to the setup, so a later change to the default export shows up as a diff of that file. Co-Authored-By: Claude Code <noreply@anthropic.com>
The tracing bridge leaves a log record's event time unset and the SDK fills in only the observed time, so records were exported with an event time of zero. A backend that dates records by their event time treats them as very old and can drop them while still answering with success. Add a log processor, ahead of the batch processor, that sets the event time to the observed time when a record has none. Records that already carry an event time are left untouched. The recorded baseline changes in the event time of each log record and in nothing else. Co-Authored-By: Claude Code <noreply@anthropic.com>
…standard variables Every server process reported the same identity: a fixed service name and a fixed short list of resource attributes, so an operator running many servers could not tell their telemetry apart. OTEL_SERVICE_NAME, when set, replaces the built-in service name. OTEL_RESOURCE_ATTRIBUTES is merged into the resource of traces, metrics and logs. What the server computes itself keeps precedence: runtime-id always, the environment and the version when their own variables are set, and the service name unless OTEL_SERVICE_NAME is set. Entries that are not key=value are ignored and reported in one startup warning that does not print them. With neither variable set the resource is unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
With an endpoint configured, traces, metrics and logs were all exported, so a backend that accepts only some signals still received the others. OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER and OTEL_LOGS_EXPORTER set to none now switch that signal's exporter off. Unset or otlp exports as before. Any other value is reported in one startup warning and exported as otlp. A signal that is switched off keeps its provider and gets no exporter, so spans still carry trace context and the process logs as before. The startup log line names the signals that are exported. Co-Authored-By: Claude Code <noreply@anthropic.com>
Sampling always followed the caller: a span whose caller marked the request "not sampled" was never recorded, and there was no setting to change that. OTEL_TRACES_SAMPLER now accepts always_on, always_off, parentbased_always_on (the default, as before), parentbased_always_off, traceidratio and parentbased_traceidratio, with OTEL_TRACES_SAMPLER_ARG as the ratio. A sampler that is not supported is reported in one startup warning and the default is used; a ratio that is not a number from 0 to 1 is reported and 1 is used. The name-based filter did not read OTEL_TRACES_SAMPLER_ARG and still does not. It stays around whichever sampler is chosen, so the names it drops and the reduced-rate names behave the same under every sampler. Co-Authored-By: Claude Code <noreply@anthropic.com>
With per-layer filters, asking the log bridge "is logging enabled?" without logging anything can leave filter state on the thread, and the next span on that thread is then dropped by every output. The subscriber set up here filters globally, so it is not affected: the export and the log lines after such a probe are the same as without it. The test fails when the same layers are given per-layer filters (the first span after the probe is lost), so it guards the setup against that change. Adds log as a dev-dependency of temper-observe to make the probe. It was already in the lock file as a transitive dependency. Co-Authored-By: Claude Code <noreply@anthropic.com>
…export settings A value that is not supported in OTEL_TRACES_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_LOGS_EXPORTER, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG or OTEL_RESOURCE_ATTRIBUTES is reported exactly once, on the log output and as an exported log record, the server still starts, and the default applies. An empty value is the same as an unset variable. The credential in the OTLP headers reaches the listener on every signal and appears in no output. Co-Authored-By: Claude Code <noreply@anthropic.com>
The export test file had grown past 500 lines. Move the identity, signal switch, sampler and bad-value tests into modules of their own and share one baseline assertion. No test is added, removed or changed. Co-Authored-By: Claude Code <noreply@anthropic.com>
Add OTEL_SERVICE_NAME, OTEL_RESOURCE_ATTRIBUTES, the three OTEL_*_EXPORTER variables, OTEL_TRACES_SAMPLER and OTEL_TRACES_SAMPLER_ARG to the module documentation and to the environment variable appendix of the guide, with their defaults, the precedence of computed resource attributes, and what happens to a value that is not supported. Co-Authored-By: Claude Code <noreply@anthropic.com>
bvolpato
approved these changes
Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
temper servecan export traces, metrics and logs over OTLP/HTTP. Running it as one of many server processes, against an OTLP backend that is stricter than a typical collector, showed four gaps. This closes them using the standard OpenTelemetry environment variables. With none of the new variables set, the startup output and the export are unchanged apart from the event time on log records.OTEL_SERVICE_NAMEnow replaces the built-in service name andOTEL_RESOURCE_ATTRIBUTESis merged into the resource of all three signals.OTEL_TRACES_EXPORTER,OTEL_METRICS_EXPORTERandOTEL_LOGS_EXPORTERset tononenow switch that signal's exporter off.OTEL_TRACES_SAMPLERnow chooses the sampler. The name-based filter stays around whichever sampler is chosen.All changes are in
crates/temper-observe. The call site intemper servedid not need to change: it still callsinit_observability("temper-platform").New variables
OTEL_SERVICE_NAMEtemper-platformfortemper serve)OTEL_RESOURCE_ATTRIBUTESkey=value,key=valueon traces, metrics and logs. Before this change onlydeployment.environment.namewas read from it.OTEL_TRACES_EXPORTERotlpnoneswitches the export of traces off.OTEL_METRICS_EXPORTERotlpnoneswitches the export of metrics off.OTEL_LOGS_EXPORTERotlpnoneswitches the export of logs off.OTEL_TRACES_SAMPLERparentbased_always_onalways_on,always_off,traceidratio,parentbased_always_on,parentbased_always_offorparentbased_traceidratio.OTEL_TRACES_SAMPLER_ARG1Behaviour worth a reviewer's attention:
OTEL_RESOURCE_ATTRIBUTES:runtime-idalways,deployment.environment.namewhenDD_ENVorLOGFIRE_ENVIRONMENTis set,service.versionwhenDD_VERSIONis set. Aservice.nameinsideOTEL_RESOURCE_ATTRIBUTESdoes not replace the built-in name; onlyOTEL_SERVICE_NAMEdoes. The OpenTelemetry specification would let it; this is deliberate, so that a deployment that already carries it there keeps its service name.OTEL_RESOURCE_ATTRIBUTESthat are notkey=valueare counted in the warning and not printed.OTEL_TRACES_SAMPLERnow takes effect. Until now it was ignored, because the setup always installed its own sampler. A deployment that already has it in its environment will see it applied.remote_parent_context) marks every remote parent as sampled, and that is unchanged here.Not done
opentelemetry-otlp0.28.0) checks only the status code and drops the body, insrc/exporter/http/{trace,metrics,logs}.rs, so there is nothing to report from. Doing it would mean wrapping the exporter's HTTP client and decoding the response here, with new direct dependencies. Left out.logbridge "is logging enabled?" without logging can leave filter state on the thread, and the next span on that thread is dropped. The subscriber here filters globally, so it is not affected. The test is kept as a guard: it fails if the same layers are given per-layer filters.How it is tested
crates/temper-observe/tests/otel_exportstarts the real setup (init_observability) in a child process for each case, with a clean environment plus the variables under test, pointed at a local OTLP/HTTP listener that records every request. The tests assert on the decoded requests and on what the child printed. No protobuf dependency is added; the tests read the few fields they need from the wire format.default_export.baseline.txtis the startup output and the full export with no new variable set. It was recorded in the first commit, before any change to the setup. The only change to it in this pull request is the event time of each log record (time=<zero>totime=<observed time>).Acceptance checks
All commands were run locally on the final commit of this branch.
cargo test -p temper-observeruns 75 unit tests and the 24 tests ofotel_export; all pass (cargo nextest run -p temper-observe: 99 run, 99 passed).every_log_record_has_an_event_time. Before the fix:log record "test log inside a span" was exported with an event time of zero, left: 0, right: 0. After it: passes. Against a runningtemper serve: 466 log records, 0 with an event time of zero.OTEL_SERVICE_NAME=xgivesservice.name = x; unset gives the built-in nameidentity::service_name_follows_otel_service_name,identity::service_name_is_built_in_when_unset, on all three signals.OTEL_RESOURCE_ATTRIBUTES=a=1,b=2is on traces, metrics and logs;runtime-idstill present; computed attributes not overriddenidentity::resource_attributes_are_merged_on_every_signal,identity::computed_resource_attributes_keep_precedence,identity::resource_attributes_supply_what_has_no_variable_of_its_own.OTEL_METRICS_EXPORTER=none: traces and logs arrive, no metrics request; same for the other two signalssignals::metrics_exporter_none_switches_only_metrics_off,signals::traces_exporter_none_switches_only_traces_off,signals::logs_exporter_none_switches_only_logs_off. Against a runningtemper servewithOTEL_METRICS_EXPORTER=none: 3 trace requests, 3 log requests and no metrics request in 40 s (the metric interval is 30 s).OTEL_TRACES_SAMPLER=always_on: a request marked "not sampled" still produces a span with the caller's trace ID; unset, it produces nonesampling::always_on_records_a_request_marked_not_sampled,sampling::default_sampler_follows_a_caller_that_did_not_sample. Tested with a span whose remote parent is marked not sampled; see the note on the server's request path above.traceidratiowith argument0produces no spans, with1allsampling::traceidratio_zero_exports_no_spans,sampling::traceidratio_one_exports_every_span.sampling::every_sampler_keeps_the_name_based_filter: nine sampler and ratio combinations; under each,clock_time_get,turso.configured_connectionand the reduced-rate name on a trace ID outside the rate are not exported. The existing unit tests of the filter are unchanged and pass.bad_values::unsupported_exporter_is_reported_once_and_exported(all three variables),bad_values::unsupported_sampler_is_reported_once_and_the_default_is_used,bad_values::bad_sampler_ratio_is_reported_once_and_one_is_used,bad_values::malformed_resource_attributes_are_reported_once_and_ignored,bad_values::empty_values_are_the_same_as_unset.OTEL_SERVICE_NAMEtakes any non-empty text, so it has no unknown value.default_export_matches_recorded_baseline. The baseline was recorded in the first commit, before any change; its only diff in this pull request istime=<zero>totime=<observed time>on the four log records.log_enabled_probe_does_not_drop_the_next_span. It fails when the layers are given per-layer filters, withtest.requestmissing from the export.cargo fmt --check,cargo check --workspace,cargo clippy --workspace --all-targets -- -D warnings, the readability ratchet, the storage dispatch boundary check, the TODO andunwrap()scans,check_instrumentationandcargo test --doc --workspaceall pass.cargo nextest run --workspace -E 'not test(dst_)', the command CI uses: 3505 tests run, 3505 passed, 65 skipped. CI has not run yet.temper_observe::oteland "Telemetry export settings" in Appendix C ofdocs/AGENT_GUIDE.md.Also covered, from the constraints:
bad_values::credentials_are_sent_and_never_logged(the credential inOTEL_EXPORTER_OTLP_HEADERSreaches the listener on every signal and appears in no output), and a run of the builttemper servewithOTEL_SERVICE_NAME,OTEL_RESOURCE_ATTRIBUTES,OTEL_METRICS_EXPORTER=none,OTEL_TRACES_SAMPLER=always_onand an unsupportedOTEL_LOGS_EXPORTER: it started, logged one warning, and exported traces and logs under the chosen name and attributes.Lint baseline against the result
mainatf66287dcargo fmt --checkcargo clippy --workspace --all-targets -- -D warningsPROD_PRINTLN_COUNT245,PROD_FILES_GT50078)PROD_FILES_GT300Departures from the plan
tracing::warn!after the subscriber is installed, not througheprintln!like the existing endpoint messages. They are then exported as log records too, and the readability ratchet'sprintlncount does not grow.log, intemper-observe, to make the "is logging enabled?" probe through the reallogbridge. It was already in the lock file as a transitive dependency;Cargo.lockgains one line.temper serve. It is the same functiontemper servecalls, with the same service name.🤖 Generated with Claude Code
The PR appears safe to merge; no actionable issue was established in the changed code.
Summary
The PR adds OpenTelemetry environment-based resource identity, per-signal export controls, and sampler selection, and fills in missing event times on exported logs.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR Env[OTEL environment variables] --> Settings[ExportSettings] Settings --> Resource[Shared resource] Settings --> Sampler[Name-based filter and chosen sampler] Settings --> Signals{Signal enabled?} Sampler --> Traces[Tracer provider] Resource --> Traces Resource --> Metrics[Meter provider] Resource --> Logs[Logger provider] Signals -->|yes| Exporters[OTLP exporters] Logs --> EventTime[Fill missing event time] EventTime --> ExportersReviews (1) · Last reviewed commit: "docs(observe): list the telemetry export..."