Skip to content

Commit 0709ae2

Browse files
refactor(libsy): adopt #[tracing::instrument] for method spans
Signed-off-by: Eric Liu <zengyuanl@nvidia.com>
1 parent 8c31505 commit 0709ae2

5 files changed

Lines changed: 66 additions & 88 deletions

File tree

‎Cargo.lock‎

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/libsy/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ rand = "0.8"
2222
switchyard-protocol = { path = "../protocol" }
2323
tokio = { version = "1", features = ["full"] }
2424
tokio-stream = "0.1"
25-
tracing = { version = "0.1", default-features = false, features = ["std"] }
25+
tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] }
2626

2727
[dev-dependencies]
2828
# SDK + in-memory exporter to assert what the observability layer records.

‎crates/libsy/src/core/algorithm.rs‎

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! routing/optimization algorithm implements, and the offload channel it makes model
66
//! calls and publishes [`Decision`]s over. See the crate root for the narrative model.
77
8-
use std::{error::Error, pin::Pin, sync::Arc};
8+
use std::{error::Error, pin::Pin, sync::Arc, time::Instant};
99

1010
use async_trait::async_trait;
1111
use futures::{Stream, StreamExt};
@@ -133,16 +133,37 @@ impl Driver {
133133
/// response resolves when its stream handle arrives); latency, outcome, and
134134
/// token usage are recorded when it resolves. The provider call itself gets a
135135
/// `libsy.client_call` span when [`Algorithm::run`] serves it.
136+
#[tracing::instrument(
137+
target = "libsy",
138+
name = "libsy.llm_call",
139+
skip_all,
140+
fields(
141+
algorithm = observability::algorithm_label(&routed.ctx),
142+
selected_model = routed.decision.selected_model(),
143+
outcome = tracing::field::Empty,
144+
error = tracing::field::Empty,
145+
input_tokens = tracing::field::Empty,
146+
output_tokens = tracing::field::Empty,
147+
total_tokens = tracing::field::Empty,
148+
reasoning_tokens = tracing::field::Empty,
149+
)
150+
)]
136151
pub async fn call_llm(&self, routed: RoutedRequest) -> Result<Response, BoxErr> {
137-
let ctx = routed.ctx.clone();
152+
let algorithm = observability::algorithm_label(&routed.ctx).to_string();
138153
let selected_model = routed.decision.selected_model().to_string();
139-
observability::observe_llm_call(
140-
&ctx,
154+
let started = Instant::now();
155+
let result = self
156+
.driver
157+
.fulfill_request::<RoutedRequest, Response>(routed.ctx.clone(), routed)
158+
.await;
159+
observability::record_llm_call(
160+
&algorithm,
141161
&selected_model,
142-
self.driver
143-
.fulfill_request::<RoutedRequest, Response>(routed.ctx.clone(), routed),
144-
)
145-
.await
162+
started.elapsed(),
163+
&result,
164+
&tracing::Span::current(),
165+
);
166+
result
146167
}
147168

148169
/// Offload a call to `target`: pair `request` with `decision` and the target's
@@ -393,6 +414,19 @@ where
393414
// Serve one offloaded call with its target's default client. A failed *model*
394415
// call is forwarded to the algorithm via `respond`; this errors only on an
395416
// infrastructure failure (no default client, or the promise was dropped).
417+
// `serve` makes the one API call libsy itself performs, so it gets its
418+
// own `libsy.client_call` span.
419+
#[tracing::instrument(
420+
target = "libsy",
421+
name = "libsy.client_call",
422+
skip_all,
423+
fields(
424+
algorithm = observability::algorithm_label(&call.get_routed().ctx),
425+
selected_model = call.get_decision().selected_model(),
426+
outcome = tracing::field::Empty,
427+
error = tracing::field::Empty,
428+
)
429+
)]
396430
async fn serve(call: CallLlmRequest) -> Result<(), Box<dyn Error + Send + Sync>> {
397431
let routed = call.get_routed().clone();
398432
let client = routed.default_client.clone().ok_or_else(|| {
@@ -425,15 +459,7 @@ where
425459
match step {
426460
None => stream_open = false,
427461
Some(item) => match item? {
428-
Step::CallLlm(call) => {
429-
// `serve` makes the one API call libsy itself
430-
// performs; give it its own client-call span.
431-
let span = observability::client_call_span(
432-
&call.get_routed().ctx,
433-
call.get_decision().selected_model(),
434-
);
435-
in_flight.push(serve(*call).instrument(span));
436-
}
462+
Step::CallLlm(call) => in_flight.push(serve(*call)),
437463
Step::Decision(decision) => trace.push(decision),
438464
Step::ReturnToAgent(response) => {
439465
final_response = Some(*response);

‎crates/libsy/src/observability.rs‎

Lines changed: 6 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
//! no-op. Spans and logs use the `tracing` facade (the async-native surface the
1313
//! OpenTelemetry ecosystem bridges with `tracing-opentelemetry` /
1414
//! `opentelemetry-appender-tracing`), so the host's subscriber decides where
15-
//! they go. Spans are attached to futures with [`Instrument`], never a
16-
//! [`Span::enter`] guard held across an `.await`: a suspended task would leave
15+
//! they go. Method spans use `#[tracing::instrument]`; the `libsy.run` span is
16+
//! attached to the spawned run task with [`tracing::Instrument`]. Neither holds
17+
//! a [`Span::enter`] guard across an `.await` — a suspended task would leave
1718
//! the span entered on its executor thread, mis-parenting every span other
1819
//! tasks create there (see the `tracing` docs on spans in asynchronous code).
1920
//!
@@ -34,7 +35,7 @@ use std::time::{Duration, Instant};
3435

3536
use opentelemetry::metrics::Meter;
3637
use opentelemetry::{global, KeyValue};
37-
use tracing::{Instrument, Span};
38+
use tracing::Span;
3839

3940
use crate::{Context, Decision, Metadata, Response};
4041

@@ -49,7 +50,7 @@ const SCOPE: &str = "libsy";
4950
pub(crate) const ALGORITHM_KEY: &str = "algorithm";
5051

5152
/// The algorithm label carried by a request context; empty until stamped.
52-
fn algorithm_label<S>(ctx: &Context<S>) -> &str {
53+
pub(crate) fn algorithm_label<S>(ctx: &Context<S>) -> &str {
5354
ctx.values
5455
.get(ALGORITHM_KEY)
5556
.map(String::as_str)
@@ -126,48 +127,6 @@ pub(crate) async fn observe_run<S>(
126127
result
127128
}
128129

129-
/// Drives one offloaded model call inside its own `libsy.llm_call` span,
130-
/// recording the call counter, latency histogram, token usage, span fields,
131-
/// and failure log when the call resolves.
132-
pub(crate) async fn observe_llm_call(
133-
ctx: &Context,
134-
selected_model: &str,
135-
call: impl Future<Output = Result<Response, BoxErr>>,
136-
) -> Result<Response, BoxErr> {
137-
let algorithm = algorithm_label(ctx);
138-
let span = llm_call_span(algorithm, selected_model);
139-
async {
140-
let started = Instant::now();
141-
let result = call.await;
142-
record_llm_call(
143-
algorithm,
144-
selected_model,
145-
started.elapsed(),
146-
&result,
147-
&Span::current(),
148-
);
149-
result
150-
}
151-
.instrument(span)
152-
.await
153-
}
154-
155-
/// Span covering one *actual* provider API call the crate itself performs —
156-
/// the default-client serve path inside [`Algorithm::run`](crate::Algorithm::run).
157-
/// `libsy.llm_call` measures fulfillment as the algorithm observes it; this
158-
/// span isolates the client call that fulfills it. A host serving calls over
159-
/// its own transport should emit an equivalent span in its `LlmClient`.
160-
pub(crate) fn client_call_span(ctx: &Context, selected_model: &str) -> Span {
161-
tracing::info_span!(
162-
target: SCOPE,
163-
"libsy.client_call",
164-
algorithm = algorithm_label(ctx),
165-
selected_model,
166-
outcome = tracing::field::Empty,
167-
error = tracing::field::Empty,
168-
)
169-
}
170-
171130
/// Records the outcome fields on the enclosing `libsy.client_call` span. The
172131
/// failure itself is not logged here — it propagates to the algorithm, where
173132
/// the `libsy.llm_call` recording logs it once.
@@ -179,26 +138,6 @@ pub(crate) fn record_client_call(result: &Result<Response, BoxErr>) {
179138
}
180139
}
181140

182-
/// Span covering one offloaded model call, a child of the surrounding
183-
/// `libsy.run` span. It measures *fulfillment* as the algorithm observes it —
184-
/// host queueing and serving included, not just the provider call. `outcome`,
185-
/// `error`, and the token-count fields are filled in by [`record_llm_call`]
186-
/// when the call resolves.
187-
fn llm_call_span(algorithm: &str, selected_model: &str) -> Span {
188-
tracing::info_span!(
189-
target: SCOPE,
190-
"libsy.llm_call",
191-
algorithm,
192-
selected_model,
193-
outcome = tracing::field::Empty,
194-
error = tracing::field::Empty,
195-
input_tokens = tracing::field::Empty,
196-
output_tokens = tracing::field::Empty,
197-
total_tokens = tracing::field::Empty,
198-
reasoning_tokens = tracing::field::Empty,
199-
)
200-
}
201-
202141
/// Records the end of one algorithm run: the run counter and duration
203142
/// histogram, the `outcome`/`error` fields on `span`, and a warn log when the
204143
/// run failed.
@@ -226,7 +165,7 @@ fn record_run(algorithm: &str, duration: Duration, result: &Result<Response, Box
226165
/// latency histogram, token counters from the response usage (absent fields are
227166
/// skipped, not recorded as zero), the `outcome`/`error`/token fields on
228167
/// `span`, and a warn log when the call failed.
229-
fn record_llm_call(
168+
pub(crate) fn record_llm_call(
230169
algorithm: &str,
231170
selected_model: &str,
232171
duration: Duration,

‎crates/libsy/tests/observability.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,10 @@ async fn successful_run_records_metrics_spans_and_decision_log() -> Result<(), B
453453
// The default-client serve inside `run` gets its own client-call span.
454454
let client_span = find_span(&spans, "libsy.client_call", "selected_model", MODEL);
455455
// Host-side span: `run`'s serve loop creates it outside the algorithm's
456-
// spans, so it has no libsy parent. This pins the `Future::instrument`
457-
// idiom — an `Entered` guard held across the offload `.await` would leave
458-
// `libsy.llm_call` entered on the thread and leak it in as the parent.
456+
// spans, so it has no libsy parent. This pins the instrument idiom
457+
// (`#[tracing::instrument]` / `Future::instrument`) — an `Entered` guard
458+
// held across the offload `.await` would leave `libsy.llm_call` entered
459+
// on the thread and leak it in as the parent.
459460
assert_eq!(client_span.parent.as_deref(), None);
460461
assert_eq!(
461462
client_span.fields.get("algorithm").map(String::as_str),

0 commit comments

Comments
 (0)