Repository navigation
otlprelay: forward log records - #1800
Conversation
The relay carried traces and metrics. ateom is about to emit log records over it, so it forwards LogsService the same way: source gate, verbatim payload, atelet's own headers upstream.
Krisztian F (krisztianfekete)
left a comment
There was a problem hiding this comment.
Added some questions/comments!
| headersEnv = "OTEL_EXPORTER_OTLP_HEADERS" | ||
| tracesHeadersEnv = "OTEL_EXPORTER_OTLP_TRACES_HEADERS" | ||
| metricsHeadersEnv = "OTEL_EXPORTER_OTLP_METRICS_HEADERS" | ||
| logsHeadersEnv = "OTEL_EXPORTER_OTLP_LOGS_HEADERS" |
There was a problem hiding this comment.
What happens if atelet also sets OTEL_EXPORTER_OTLP_LOGS_ENDPOINT? I think upstreamTarget() (L548) only reads the generic, traces and metrics variables, so logs would quietly go to the traces/metrics collector. Same question for LOGS_COMPRESSION in upstreamCompression() (L508).
There was a problem hiding this comment.
Right, both resolvers ignored the logs variable, so logs would have followed the traces/metrics collector silently. Both now read it and apply the same conflict rule, and the error names the variables the two values came from, since a fallback pulls the generic in under a signal that is not itself set. One behavior change falls out: a config that sets the traces and metrics overrides to one collector and leaves logs unset now fails at startup, because logs falls back to the generic and that is a two-collector configuration. The old test row asserting the lenient result is updated and a new row pins the refusal.
| @@ -97,6 +98,7 @@ const ( | |||
There was a problem hiding this comment.
| // traces, metrics, and logs may legitimately differ and are resolved separately. |
There was a problem hiding this comment.
Taken.
There was a problem hiding this comment.
logRelay shares the gate too now, want to update this?
There was a problem hiding this comment.
Updated.
| collogspb.RegisterLogsServiceServer(s.grpc, &logRelay{ | ||
| upstream: collogspb.NewLogsServiceClient(upstream), | ||
| headers: logHeaders, | ||
| gate: gate, | ||
| }) |
There was a problem hiding this comment.
Just a forward thinking question for later steps: a new ateom that talks to an old atelet gets Unimplemented on logs. The SDK drops that batch without retrying, and Dial won't fall back because the socket exists I think. How were you planning for RelayCapable to handle that?
There was a problem hiding this comment.
It won't, and we don't plan to make it. An old atelet returns Unimplemented, the SDK doesn't retry it, and Dial only falls back when the socket is missing, so a new ateom on an old atelet loses its log batches for the rollout window. We accept that: the project is early and doesn't carry backward compatibility across a version skew (#1644 broke the discovery response shape in place the same way, and #809 landed the relay with the same atelet-before-ateom ordering). The window is bounded, self-heals, and costs telemetry, not the actor; RelayCapable stays a resource attribute.
| logs []*collogspb.ExportLogsServiceRequest | ||
| traceMD []metadata.MD | ||
| metricMD []metadata.MD | ||
| logMD []metadata.MD |
There was a problem hiding this comment.
Are you planning to extend exportBoth and TestExportAttachesAteletHeaders with a logs coverage? That's the path where we most want proof that an ateom can't pick atelet's credentials.
There was a problem hiding this comment.
Yes. exportBoth is now exportAll with a log export, TestExportDropsClientMetadata checks the log leg too, and TestExportAttachesAteletHeaders sets OTEL_EXPORTER_OTLP_LOGS_HEADERS and asserts the log export carries it and nothing of the client's.
| } | ||
| } | ||
|
|
||
| func TestRelayRefusesNonAteomLogs(t *testing.T) { |
There was a problem hiding this comment.
Wdyt about doing this as a third Export in the TestRelayRefusesNonAteomSource table instead? Then logs also get the empty service.name case for free. Same idea for TestRelayAcceptsEveryAteomService and TestRelayRefusesMixedBatch.
There was a problem hiding this comment.
Done for all three; the separate logs test is gone, and the empty service.name case now covers logs.
| func (l *logsSink) Export(ctx context.Context, req *collogspb.ExportLogsServiceRequest) (*collogspb.ExportLogsServiceResponse, error) { | ||
| l.parent.mu.Lock() | ||
| l.parent.logs = append(l.parent.logs, req) | ||
| if md, ok := metadata.FromIncomingContext(ctx); ok { | ||
| l.parent.logMD = append(l.parent.logMD, md.Copy()) | ||
| } | ||
| l.parent.mu.Unlock() | ||
| l.parent.got <- struct{}{} | ||
| return &collogspb.ExportLogsServiceResponse{}, nil | ||
| } |
There was a problem hiding this comment.
If you add a LOGS_ENDPOINT conflict check, would a logs row in TestUpstreamTarget be the natural place to check this?
There was a problem hiding this comment.
Yes; three logs rows there (agrees, conflicts with generic, conflicts with traces) plus the row where logs is unset and falls back to the generic, and two in the compression table.
| carries logs, traces, and metrics, so a worker pod exports log records the same | ||
| way it exports the other two signals. |
There was a problem hiding this comment.
Technically, worker pods will only export logs later, right?
There was a problem hiding this comment.
Right. Split into two facts: the relay carries logs, traces, and metrics; worker pods do not emit log records yet.
The relay read the generic, traces, and metrics OTLP variables and ignored the logs ones, so a logs-specific collector or compression setting was silently overridden by whatever traces and metrics resolved to. All three signals now go through one resolver that requires every resolved value to agree, and its conflict error names the variable each value came from, since a signal that falls back to the generic pulls it in under a variable that is not itself set. Also extends the source-gate, header, and mixed-batch tests to the log path.
Krisztian F (krisztianfekete)
left a comment
There was a problem hiding this comment.
This LGTM, can you please update the PR description to reflect the new changes?
Thanks! Description updated. |
…bstrate#1857) Step 2 of agent-substrate#1748, plus the consumer rule from step 1b. `LoggingOptions` gains `ExporterConn` and `RelayCapable`, mirroring `TracingOptions`. Both ateoms initialize logging after metrics with the relay connection and defer a nil-guarded shutdown. `OTEL_LOGS_EXPORTER` still defaults to `none`, so no ateom emits a record; this is the provider the actor events land on when emission moves. A relay-capable log resource carries `ate.otlp.relay` like traces and metrics do. `newLoggerProvider` is split from `InitLogging` so the test asserts that on an emitted record's resource, the way `TestMeterProviderRelayAttribute` does for metrics. `docs/observability.md` states the consumer rule that replaced the relay denylist dropped in agent-substrate#1800: a lifecycle record is authoritative only under ateapi's resource, because the relay admits only ateom resources. Follow-up for the emission step, not here: the controller does not inject `OTEL_LOGS_EXPORTER` into worker pods, so no environment exercises log-over-relay yet.
Step 1 of #1748. The relay forwards
LogsServicethe way it forwards traces and metrics: source gate, verbatim payload, atelet's ownOTEL_EXPORTER_OTLP_LOGS_HEADERSupstream.No content check on the records. Identity spoofing through the relay is handled in #1748 by the consumer rule (a lifecycle record counts only under ateapi's resource, which the source gate already enforces) and per-pod sockets (#741), not by a denylist here.
Endpoint and compression resolution (second commit, from review). The relay read the generic, traces, and metrics OTLP variables and ignored the logs ones, so a logs-specific collector or compression setting was silently overridden. All three signals now go through one resolver: each falls back to the generic, and every resolved value must agree, since the relay has one upstream connection. The conflict error names the variable each value came from; a signal that falls back names the generic variable, not its own unset one.
One behavior change:
traces=A, metrics=A, generic=Bwith logs unset used to pass and now fails at atelet startup, because logs falls back toBand that is a two-collector configuration.Tests: an ateom log batch arrives with the ateom's resource; the source gate, mixed-batch, header-isolation, and per-signal header tests cover logs; resolver tables gained logs rows including the fallback case. Docs say the relay carries logs, traces, and metrics.