You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(ocsf): add trace_id/span_id correlation fields to OCSF event builders #2640
As a security or compliance reviewer investigating a policy violation, I want each OCSF event to carry the ID of the trace that produced it, so that I can jump from the event in my log aggregator straight to the supervisor's trace instead of matching sandbox IDs and timestamps by hand.
Problem Statement
Open Cybersecurity Schema Framework (OCSF) security events and OpenTelemetry (OTel) traces exist in separate systems with no connection between them. A security reviewer filters the OCSF log aggregator (Loki, Splunk) for DENY events in the last hour and finds a network deny for api.suspicious.com from sandbox sb-abc123. To see how the supervisor handled that connection (which policy matched, what L7 enforcement and middleware ran), they search Jaeger for the sandbox ID and hope the timestamps line up. Nothing links the security event to its trace.
Today only the gateway emits OTel spans. Once #3977 lands, the supervisor does too, and most OCSF events and OTel spans then come from the same code paths. The OCSF builders have no way to record the active trace context.
Impact / Why This Matters
Every deny investigation needs manual correlation across two systems. The workaround is searching the trace backend by sandbox ID within a time window. That breaks down quickly: with #3977 every egress connection starts its own trace, so a busy sandbox produces many traces per second and the time window rarely identifies one. SIEM pipelines also have no join key to automate the correlation.
Proposed Design
Record the active trace context in OCSF events using the OCSF 1.8 Trace profile. When a sampled OTel span is active at emission time, the event carries trace.uid and lists trace in metadata.profiles. Without one, the trace object is omitted. The reviewer pastes trace.uid into Jaeger or Tempo and lands on the supervisor trace that produced the event.
Representation
OCSF 1.8 has no top-level trace_id or span_id attributes. It defines a Trace profile whose trace object carries the W3C trace ID in trace.uid, plus an optional span object:
The first version populates trace.uid only. The 1.8 span object requires start_time and end_time, which are unknown while the span is still open at emission time. Whether and how to fill trace.span is settled during implementation against the vendored-schema validation tests. The Trace profile schema files get vendored next to the existing ai_operation profile.
Layering
openshell-ocsf stays independent of OpenTelemetry:
openshell-ocsf adds a plain-data trace correlation type and a builder setter that renders the trace object and adds the profile.
openshell-otel provides a function that extracts the active OTel context and returns it only when the span context is valid and sampled. An unsampled trace ID points to nothing in the trace backend.
Binaries that emit OCSF events register this extractor with openshell-ocsf at startup. ocsf_emit! fills in the trace context when the builder has not set one, so individual call sites need no OTel dependency. An explicit setter call overrides the automatic value.
Egress deny spans
The supervisor egress spans from #3977 are DEBUG, and the supervisor's OTLP filter resolves to openshell=info at the default warn log level. Without a change, network denies get no trace ID at default settings. The proposal is an INFO-level span for deny decisions only. Denies are rare and are what reviewers investigate, while allowed connections stay at DEBUG to keep span volume down.
The decision is known only after authorization returns, and every deny OCSF event is emitted after the authorization scope has ended. So the deny span is opened in the deny branch and entered around the OCSF emission at every deny site: L4 policy denials, mapping and SSRF denials, and L7 HTTP denials in the relay and middleware. It carries the decision attributes (policy, reason, destination). A span that wraps only the policy evaluation would leave the emission hook without a trace context.
At the default log level the DEBUG connect span is disabled, so the deny span has no exported parent and becomes a one-span root trace. trace.uid still resolves, but the full connect, authorize, resolve and dial tree appears only at debug level, where the deny span nests under the connect span.
Out of scope: agent trace context
Each egress connection starts its own root trace, so the linked trace shows the supervisor's handling of the connection, not the agent's activity. Linking a deny to the agent's own trace, through the workload's traceparent header relayed via #3196, is a follow-up. The workload controls that header, so it goes in a separate, clearly labelled field and never in the trace object.
Scope
crates/openshell-ocsf/: trace correlation type, builder setter, trace serialization, metadata.profiles entry, vendored Trace profile schema, emission hook, and downgrade stripping of the trace object and Trace profile for OCSF 1.3 and 1.1 (the profile first appears in 1.4)
OCSF events emitted inside a sampled OTel span include trace.uid (32 lowercase hex characters) and list trace in metadata.profiles.
OCSF events emitted without an active sampled span omit the trace object and do not list the profile.
At the default supervisor log level, OCSF network and HTTP deny events carry a trace.uid that resolves to an exported trace containing the deny span.
Events downgraded to OCSF 1.3 or 1.1 contain neither the trace object nor trace in metadata.profiles, covered by regression tests next to the existing downgrade tests.
openshell-ocsf has no OpenTelemetry dependency.
The Trace profile schema is vendored, and the schema validation tests cover events with and without trace.
The published observability docs describe the trace field and how to follow it to the trace backend.
Alternatives Considered
Top-level trace_id / span_id fields: Easy to query, but outside the OCSF 1.8 schema. Consumers that validate against OCSF treat them as unknown attributes. The standard Trace profile covers the same need.
The unmapped mechanism: Schema-valid, but unmapped is meant for source data that has no OCSF attribute. Trace context has one, and downstream tooling won't look for it in unmapped.
Per-site .trace_context() calls: Explicit, but every emitting crate would need an OTel dependency, and adoption would drift across the call sites. The emission hook covers every event, and the explicit setter stays available where the current span is the wrong source.
Enrichment in the JSONL layer: Works, but puts the trace context into one output format instead of the event itself, so the shorthand format and any future sink would miss it.
Raising all egress spans to INFO: Gives every OCSF network event a trace ID, but exports one trace per outbound connection at default settings. Scoping the INFO span to denies covers the investigation case at a fraction of the volume.
Agent Investigation
OCSF builders live in crates/openshell-ocsf/src/builders/. Each builder declares its profiles through ctx.metadata(&[...]), and api_activity.rs already adds ai_operation conditionally.
The vendored schemas cover OCSF 1.8.0 but include only the ai_operation profile. The Trace profile is available from the OCSF schema server.
OCSF events are emitted via ocsf_emit!(), which stores the event in a thread-local and emits via tracing::info!(). The shorthand and JSONL layers extract it from there.
openshell-otel owns OTel context handling via tracing_opentelemetry::OpenTelemetrySpanExt. The supervisor crates have no OTel dependency on main.
ocsf_emit! call sites exist in openshell-sandbox, openshell-server, openshell-supervisor, openshell-supervisor-network, and openshell-supervisor-process.
User Story
As a security or compliance reviewer investigating a policy violation, I want each OCSF event to carry the ID of the trace that produced it, so that I can jump from the event in my log aggregator straight to the supervisor's trace instead of matching sandbox IDs and timestamps by hand.
Problem Statement
Open Cybersecurity Schema Framework (OCSF) security events and OpenTelemetry (OTel) traces exist in separate systems with no connection between them. A security reviewer filters the OCSF log aggregator (Loki, Splunk) for DENY events in the last hour and finds a network deny for
api.suspicious.comfrom sandboxsb-abc123. To see how the supervisor handled that connection (which policy matched, what L7 enforcement and middleware ran), they search Jaeger for the sandbox ID and hope the timestamps line up. Nothing links the security event to its trace.Today only the gateway emits OTel spans. Once #3977 lands, the supervisor does too, and most OCSF events and OTel spans then come from the same code paths. The OCSF builders have no way to record the active trace context.
Impact / Why This Matters
Every deny investigation needs manual correlation across two systems. The workaround is searching the trace backend by sandbox ID within a time window. That breaks down quickly: with #3977 every egress connection starts its own trace, so a busy sandbox produces many traces per second and the time window rarely identifies one. SIEM pipelines also have no join key to automate the correlation.
Proposed Design
Record the active trace context in OCSF events using the OCSF 1.8 Trace profile. When a sampled OTel span is active at emission time, the event carries
trace.uidand liststraceinmetadata.profiles. Without one, thetraceobject is omitted. The reviewer pastestrace.uidinto Jaeger or Tempo and lands on the supervisor trace that produced the event.Representation
OCSF 1.8 has no top-level
trace_idorspan_idattributes. It defines a Trace profile whosetraceobject carries the W3C trace ID intrace.uid, plus an optionalspanobject:{"class_uid": 4001, "activity_id": 1, "metadata": {"profiles": ["security_control", "network_proxy", "container", "host", "trace"], ...}, "trace": {"uid": "0af7651916cd43dd8448eb211c80319c"}, ...}The first version populates
trace.uidonly. The 1.8spanobject requiresstart_timeandend_time, which are unknown while the span is still open at emission time. Whether and how to filltrace.spanis settled during implementation against the vendored-schema validation tests. The Trace profile schema files get vendored next to the existingai_operationprofile.Layering
openshell-ocsfstays independent of OpenTelemetry:openshell-ocsfadds a plain-data trace correlation type and a builder setter that renders thetraceobject and adds the profile.openshell-otelprovides a function that extracts the active OTel context and returns it only when the span context is valid and sampled. An unsampled trace ID points to nothing in the trace backend.openshell-ocsfat startup.ocsf_emit!fills in the trace context when the builder has not set one, so individual call sites need no OTel dependency. An explicit setter call overrides the automatic value.Egress deny spans
The supervisor egress spans from #3977 are DEBUG, and the supervisor's OTLP filter resolves to
openshell=infoat the defaultwarnlog level. Without a change, network denies get no trace ID at default settings. The proposal is an INFO-level span for deny decisions only. Denies are rare and are what reviewers investigate, while allowed connections stay at DEBUG to keep span volume down.The decision is known only after authorization returns, and every deny OCSF event is emitted after the authorization scope has ended. So the deny span is opened in the deny branch and entered around the OCSF emission at every deny site: L4 policy denials, mapping and SSRF denials, and L7 HTTP denials in the relay and middleware. It carries the decision attributes (policy, reason, destination). A span that wraps only the policy evaluation would leave the emission hook without a trace context.
At the default log level the DEBUG connect span is disabled, so the deny span has no exported parent and becomes a one-span root trace.
trace.uidstill resolves, but the full connect, authorize, resolve and dial tree appears only at debug level, where the deny span nests under the connect span.Out of scope: agent trace context
Each egress connection starts its own root trace, so the linked trace shows the supervisor's handling of the connection, not the agent's activity. Linking a deny to the agent's own trace, through the workload's
traceparentheader relayed via #3196, is a follow-up. The workload controls that header, so it goes in a separate, clearly labelled field and never in thetraceobject.Scope
crates/openshell-ocsf/: trace correlation type, builder setter,traceserialization,metadata.profilesentry, vendored Trace profile schema, emission hook, and downgrade stripping of thetraceobject and Trace profile for OCSF 1.3 and 1.1 (the profile first appears in 1.4)crates/openshell-otel/: sampled trace context extractioncrates/openshell-supervisor-network/: INFO-level deny span, entered around the OCSF emission at every deny sitetraceobject is optional)Dependencies
Acceptance Criteria
trace.uid(32 lowercase hex characters) and listtraceinmetadata.profiles.traceobject and do not list the profile.trace.uidthat resolves to an exported trace containing the deny span.traceobject nortraceinmetadata.profiles, covered by regression tests next to the existing downgrade tests.openshell-ocsfhas no OpenTelemetry dependency.trace.tracefield and how to follow it to the trace backend.Alternatives Considered
Top-level
trace_id/span_idfields: Easy to query, but outside the OCSF 1.8 schema. Consumers that validate against OCSF treat them as unknown attributes. The standard Trace profile covers the same need.The
unmappedmechanism: Schema-valid, butunmappedis meant for source data that has no OCSF attribute. Trace context has one, and downstream tooling won't look for it inunmapped.Per-site
.trace_context()calls: Explicit, but every emitting crate would need an OTel dependency, and adoption would drift across the call sites. The emission hook covers every event, and the explicit setter stays available where the current span is the wrong source.Enrichment in the JSONL layer: Works, but puts the trace context into one output format instead of the event itself, so the shorthand format and any future sink would miss it.
Raising all egress spans to INFO: Gives every OCSF network event a trace ID, but exports one trace per outbound connection at default settings. Scoping the INFO span to denies covers the investigation case at a fraction of the volume.
Agent Investigation
crates/openshell-ocsf/src/builders/. Each builder declares its profiles throughctx.metadata(&[...]), andapi_activity.rsalready addsai_operationconditionally.ai_operationprofile. The Trace profile is available from the OCSF schema server.ocsf_emit!(), which stores the event in a thread-local and emits viatracing::info!(). The shorthand and JSONL layers extract it from there.openshell-otelowns OTel context handling viatracing_opentelemetry::OpenTelemetrySpanExt. The supervisor crates have no OTel dependency on main.ocsf_emit!call sites exist inopenshell-sandbox,openshell-server,openshell-supervisor,openshell-supervisor-network, andopenshell-supervisor-process.supervisor.egress.connect,authorize,resolve,dial) are DEBUG, and the OTLP filter isinfo,openshell=<level>with a floor of INFO.Related: #1055 (Enterprise Observability), #2508 (Supervisor OTel span emission), #3977 (supervisor OTLP span export), #3196 (OTLP relay), #2507 (Gateway OTel export surface)