diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs
index f5731a3..ea3250f 100644
--- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs
+++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs
@@ -40,7 +40,7 @@ protected override void OnEvent(string name, object? payload)
DiagnosticPayloadReader.GetInt32(payload, "http.response.status_code", "http.status_code");
var errorType = DiagnosticPayloadReader.GetString(payload, "error.type", "exception.type");
- using var activity = QylActivitySource.Source.StartActivity(QylActivityNames.HttpServer(method, route), ActivityKind.Server);
+ using var activity = QylActivitySource.StartAtAmbientStart(QylActivityNames.HttpServer(method, route), ActivityKind.Server);
SemanticTagWriter.Set(activity, SemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.HttpServer);
SemanticTagWriter.Set(activity, SemanticAttributes.HttpRequestMethod, method);
diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs
index ac9ff2f..23fa612 100644
--- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs
+++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/DiagnosticListenerSubscriber.cs
@@ -10,8 +10,11 @@ namespace Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners;
/// Pre-swap: each library (HttpClient, EFCore, …) was instrumented by an IL-rewriting CallTarget
/// integration injected by the substrate's CLR profiler. Post-swap: we subscribe to the same
/// libraries' built-in events. DiagnosticSource is a managed BCL
-/// primitive that's been AOT-safe since .NET 8, so this layer publishes the same span shapes
-/// without any IL rewriting or runtime code generation.
+/// primitive that's been AOT-safe since .NET 8, so this layer emits spans without any IL rewriting
+/// or runtime code generation. Concrete subscribers react on the completion (*.Stop) event and
+/// stamp the span to the ambient framework activity's start (via
+/// QylActivitySource.StartAtAmbientStart) so the emitted duration reflects the real operation,
+/// not a ~0 span.
///
///
public abstract class DiagnosticListenerSubscriber : IObserver>, IDisposable
diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs
index 5fac9dd..c2fc930 100644
--- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs
+++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/GrpcClient/GrpcClientDiagnosticListener.cs
@@ -38,7 +38,7 @@ protected override void OnEvent(string name, object? payload)
DiagnosticPayloadReader.GetInt32(payload, "grpc.status_code");
var errorType = DiagnosticPayloadReader.GetString(payload, "error.type", "exception.type");
- using var activity = QylActivitySource.Source.StartActivity(QylActivityNames.GrpcClient(service, method), ActivityKind.Client);
+ using var activity = QylActivitySource.StartAtAmbientStart(QylActivityNames.GrpcClient(service, method), ActivityKind.Client);
SemanticTagWriter.Set(activity, SemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.RpcGrpc);
SemanticTagWriter.Set(activity, SemanticAttributes.RpcSystem, QylSemanticAttributes.RpcSystemGrpc);
diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs
index c937ad9..a92f1d7 100644
--- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs
+++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/HttpClient/HttpClientDiagnosticListener.cs
@@ -39,7 +39,7 @@ protected override void OnEvent(string name, object? payload)
var statusCode = DiagnosticPayloadReader.GetInt32(payload, "http.response.status_code", "http.status_code");
var errorType = DiagnosticPayloadReader.GetString(payload, "error.type", "exception.type");
- using var activity = QylActivitySource.Source.StartActivity(QylActivityNames.HttpClient(method), ActivityKind.Client);
+ using var activity = QylActivitySource.StartAtAmbientStart(QylActivityNames.HttpClient(method), ActivityKind.Client);
SemanticTagWriter.Set(activity, SemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.HttpClient);
SemanticTagWriter.Set(activity, SemanticAttributes.HttpRequestMethod, method);
diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
index d986310..4749073 100644
--- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
+++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
@@ -25,4 +25,21 @@ internal static bool IsRecordingEnabled
=> Source.HasListeners()
? Source.StartActivity(operationName, activityKind)
: null;
+
+ ///
+ /// Starts a qyl span stamped to the ambient (framework) 's real start time,
+ /// so DiagnosticListener bridges that only observe the completion (*.Stop) event emit the
+ /// operation's TRUE duration instead of a ~0 span. Parents to the current activity to preserve
+ /// trace correlation; falls back to a now-stamped span when there is no ambient activity.
+ ///
+ internal static Activity? StartAtAmbientStart(string operationName, ActivityKind activityKind)
+ {
+ if (!Source.HasListeners())
+ return null;
+
+ var ambient = Activity.Current;
+ return ambient is null
+ ? Source.StartActivity(operationName, activityKind)
+ : Source.StartActivity(operationName, activityKind, ambient.Context, tags: null, links: null, startTime: ambient.StartTimeUtc);
+ }
}
diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
index 371e11d..78a723c 100644
--- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
+++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs
@@ -47,9 +47,12 @@ private static class NetRuntimeMetrics
QylMetricNames.ProcessRuntimeDotnetGcObjectsSize,
static () => GC.GetTotalMemory(false),
"By");
- private static readonly ObservableGauge ThreadPoolThreads = Meter.CreateObservableGauge(
+ // OTel semconv: dotnet.thread_pool.thread.count is an (Observable)UpDownCounter with unit {thread},
+ // not a unitless gauge — the pool size goes up and down, and UCUM units are required.
+ private static readonly ObservableUpDownCounter ThreadPoolThreads = Meter.CreateObservableUpDownCounter(
QylMetricNames.ProcessRuntimeDotnetThreadPoolThreadsCount,
- static () => ThreadPool.ThreadCount);
+ static () => ThreadPool.ThreadCount,
+ unit: "{thread}");
private static readonly ObservableGauge ThreadPoolQueueLength = Meter.CreateObservableGauge(
QylMetricNames.ProcessRuntimeDotnetThreadPoolQueueLength,
static () => ThreadPool.PendingWorkItemCount);