Skip to content

Commit 49291eb

Browse files
committed
fix: stabilize Go OTLP scope test
Signed-off-by: Will Killian <wkillian@nvidia.com>
1 parent 1d72968 commit 49291eb

4 files changed

Lines changed: 140 additions & 27 deletions

File tree

crates/core/src/api/subscriber.rs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,18 @@ use tracing::{Event as TracingEvent, Metadata, Subscriber as TracingSubscriber};
1818
use tracing_subscriber::layer::Context as LayerContext;
1919
use uuid::Uuid;
2020

21-
pub(crate) const EVENT_TRACE_TARGET: &str = "nemo_relay::events";
22-
pub(crate) const EVENT_JSON_FIELD: &str = "event.json";
21+
/// `tracing` target used by NeMo Relay lifecycle event records.
22+
///
23+
/// External tracing layers can filter on this target before decoding records
24+
/// with [`event_from_tracing`].
25+
pub const EVENT_TRACE_TARGET: &str = "nemo_relay::events";
26+
27+
/// `tracing` field containing the canonical ATOF JSON representation.
28+
///
29+
/// The field is part of NeMo Relay's Rust tracing integration contract. Prefer
30+
/// [`event_from_tracing`] over reading this field directly when a library wants
31+
/// a canonical [`Event`].
32+
pub const EVENT_JSON_FIELD: &str = "event.json";
2333

2434
/// Callback-backed NeMo Relay event subscriber.
2535
///
@@ -53,19 +63,19 @@ impl Subscriber {
5363
}
5464

5565
fn observe_tracing_event(&self, event: &TracingEvent<'_>) {
56-
if let Some(event) = event_from_tracing_event(event) {
66+
if let Some(event) = event_from_tracing(event) {
5767
(self.callback)(&event);
5868
}
5969
}
6070
}
6171

6272
impl TracingSubscriber for Subscriber {
6373
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
64-
is_nemo_event_metadata(metadata)
74+
is_nemo_relay_event(metadata)
6575
}
6676

6777
fn register_callsite(&self, metadata: &'static Metadata<'static>) -> Interest {
68-
if is_nemo_event_metadata(metadata) {
78+
if is_nemo_relay_event(metadata) {
6979
Interest::always()
7080
} else {
7181
Interest::never()
@@ -104,12 +114,29 @@ where
104114
}
105115
}
106116

107-
pub(crate) fn is_nemo_event_metadata(metadata: &Metadata<'_>) -> bool {
117+
/// Create a tracing-subscriber layer that consumes NeMo Relay lifecycle events.
118+
///
119+
/// This is the canonical Rust integration point for libraries that want to
120+
/// receive NeMo Relay events through `tracing-subscriber` composition instead
121+
/// of registering directly in the NeMo Relay subscriber registry.
122+
pub fn tracing_layer(callback: EventSubscriberFn) -> Subscriber {
123+
Subscriber::new(callback)
124+
}
125+
126+
/// Return `true` when tracing metadata belongs to a NeMo Relay lifecycle event.
127+
///
128+
/// External layers can use this as a cheap filter before calling
129+
/// [`event_from_tracing`].
130+
pub fn is_nemo_relay_event(metadata: &Metadata<'_>) -> bool {
108131
metadata.target() == EVENT_TRACE_TARGET
109132
}
110133

111-
pub(crate) fn event_from_tracing_event(event: &TracingEvent<'_>) -> Option<Event> {
112-
if !is_nemo_event_metadata(event.metadata()) {
134+
/// Decode a canonical NeMo Relay [`Event`] from a `tracing` event record.
135+
///
136+
/// Returns `None` when the tracing event is not a NeMo Relay lifecycle record or
137+
/// when the record does not contain a valid canonical event payload.
138+
pub fn event_from_tracing(event: &TracingEvent<'_>) -> Option<Event> {
139+
if !is_nemo_relay_event(event.metadata()) {
113140
return None;
114141
}
115142

crates/core/tests/integration/api_surface_tests.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ use nemo_relay::api::runtime::{create_scope_stack, current_scope_stack, set_thre
5454
use nemo_relay::api::scope::ScopeType;
5555
use nemo_relay::api::scope::{event, pop_scope, push_scope};
5656
use nemo_relay::api::subscriber::{
57-
Subscriber as NemoSubscriber, deregister_subscriber, register_subscriber,
58-
scope_deregister_subscriber, scope_register_subscriber, scope_subscribe, subscribe,
57+
Subscriber as NemoSubscriber, deregister_subscriber, event_from_tracing, is_nemo_relay_event,
58+
register_subscriber, scope_deregister_subscriber, scope_register_subscriber, scope_subscribe,
59+
subscribe, tracing_layer,
5960
};
6061
use nemo_relay::api::tool::ToolAttributes;
6162
use nemo_relay::api::tool::{
@@ -423,7 +424,7 @@ fn test_subscriber_adapter_composes_as_tracing_subscriber_layer() {
423424

424425
let events = Arc::new(Mutex::new(Vec::new()));
425426
let sink = events.clone();
426-
let layer = NemoSubscriber::new(Arc::new(move |event| {
427+
let layer = tracing_layer(Arc::new(move |event| {
427428
sink.lock().unwrap().push(event.name().to_owned());
428429
}));
429430
let subscriber = tracing_subscriber::registry().with(layer);
@@ -444,6 +445,54 @@ fn test_subscriber_adapter_composes_as_tracing_subscriber_layer() {
444445
);
445446
}
446447

448+
#[test]
449+
fn test_public_tracing_event_decoder_supports_custom_layers() {
450+
let _lock = TEST_MUTEX.lock().unwrap();
451+
reset_global();
452+
setup_isolated_thread();
453+
454+
struct DecodingLayer {
455+
events: Arc<Mutex<Vec<String>>>,
456+
}
457+
458+
impl<S> tracing_subscriber::Layer<S> for DecodingLayer
459+
where
460+
S: tracing::Subscriber,
461+
{
462+
fn on_event(
463+
&self,
464+
event: &tracing::Event<'_>,
465+
_ctx: tracing_subscriber::layer::Context<'_, S>,
466+
) {
467+
if is_nemo_relay_event(event.metadata())
468+
&& let Some(event) = event_from_tracing(event)
469+
{
470+
self.events.lock().unwrap().push(event.name().to_owned());
471+
}
472+
}
473+
}
474+
475+
let events = Arc::new(Mutex::new(Vec::new()));
476+
let subscriber = tracing_subscriber::registry().with(DecodingLayer {
477+
events: events.clone(),
478+
});
479+
480+
tracing::subscriber::with_default(subscriber, || {
481+
event(
482+
nemo_relay::api::scope::EmitMarkEventParams::builder()
483+
.name("public-tracing-decoder-mark")
484+
.build(),
485+
)
486+
.unwrap();
487+
tracing::info!("external-event");
488+
});
489+
490+
assert_eq!(
491+
events.lock().unwrap().as_slice(),
492+
["public-tracing-decoder-mark"]
493+
);
494+
}
495+
447496
#[test]
448497
fn test_subscriber_adapter_ignores_external_tracing_events() {
449498
let _lock = TEST_MUTEX.lock().unwrap();

docs/about/concepts/subscribers.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,35 @@ adapter that implements both `tracing::Subscriber` and
6161
tracing subscriber or compose it into an existing `tracing-subscriber` registry
6262
alongside formatting, filtering, OpenTelemetry, or other host layers.
6363

64+
For Rust libraries that want to consume NeMo Relay events through tracing
65+
subscriber mechanisms, the canonical path is
66+
`nemo_relay::api::subscriber::tracing_layer(callback)`. The layer decodes NeMo
67+
Relay tracing records back into canonical `Event` values and invokes the
68+
callback. Libraries should return or document this layer for host applications
69+
to compose; they should not call `tracing::subscriber::set_global_default`.
70+
71+
```rust
72+
use std::sync::Arc;
73+
use nemo_relay::api::subscriber;
74+
use tracing_subscriber::prelude::*;
75+
76+
let nemo_events = subscriber::tracing_layer(Arc::new(|event| {
77+
// Consume the canonical NeMo Relay Event.
78+
}));
79+
80+
let tracing_stack = tracing_subscriber::registry()
81+
.with(nemo_events);
82+
83+
// Application code owns installation of the tracing subscriber.
84+
tracing::subscriber::set_global_default(tracing_stack)?;
85+
```
86+
87+
Libraries that already implement their own `tracing_subscriber::Layer` can use
88+
`subscriber::is_nemo_relay_event(metadata)` as a target filter and
89+
`subscriber::event_from_tracing(event)` to decode the canonical event payload.
90+
These helpers are the supported way to consume NeMo Relay tracing records
91+
without depending on the raw tracing field layout.
92+
6493
The Rust named APIs remain available under their original names:
6594
`register_subscriber(name, callback)`, `deregister_subscriber(name)`,
6695
`scope_register_subscriber(scope_uuid, name, callback)`, and

go/nemo_relay/otel_test.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -118,24 +118,32 @@ func TestOpenTelemetrySubscriberExportsScopeLifecycleAndMarks(t *testing.T) {
118118
}
119119
defer func() { _ = subscriber.Deregister(name) }()
120120

121-
handle, err := PushScope("otel_scope", ScopeTypeAgent)
121+
stack, err := NewScopeStack()
122122
if err != nil {
123-
t.Fatalf("PushScope failed: %v", err)
124-
}
125-
if err := EmitEvent(
126-
"otel_mark",
127-
WithEventParent(handle),
128-
WithEventData(json.RawMessage(`{"step":1}`)),
129-
WithEventMetadata(json.RawMessage(`{"source":"go"}`)),
130-
); err != nil {
131-
t.Fatalf("EmitEvent failed: %v", err)
132-
}
133-
if err := PopScope(handle); err != nil {
134-
t.Fatalf("PopScope failed: %v", err)
135-
}
136-
if err := subscriber.ForceFlush(); err != nil {
137-
t.Fatalf("ForceFlush failed: %v", err)
123+
t.Fatalf("NewScopeStack failed: %v", err)
138124
}
125+
defer stack.Close()
126+
127+
stack.Run(func() {
128+
handle, err := PushScope("otel_scope", ScopeTypeAgent)
129+
if err != nil {
130+
t.Fatalf("PushScope failed: %v", err)
131+
}
132+
if err := EmitEvent(
133+
"otel_mark",
134+
WithEventParent(handle),
135+
WithEventData(json.RawMessage(`{"step":1}`)),
136+
WithEventMetadata(json.RawMessage(`{"source":"go"}`)),
137+
); err != nil {
138+
t.Fatalf("EmitEvent failed: %v", err)
139+
}
140+
if err := PopScope(handle); err != nil {
141+
t.Fatalf("PopScope failed: %v", err)
142+
}
143+
if err := subscriber.ForceFlush(); err != nil {
144+
t.Fatalf("ForceFlush failed: %v", err)
145+
}
146+
})
139147

140148
select {
141149
case request := <-requests:

0 commit comments

Comments
 (0)