Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <see cref="DiagnosticListener"/> 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 (<c>*.Stop</c>) event and
/// stamp the span to the ambient framework activity's start (via
Comment on lines +13 to +15
/// <c>QylActivitySource.StartAtAmbientStart</c>) so the emitted duration reflects the real operation,
/// not a ~0 span.
/// </para>
/// </summary>
public abstract class DiagnosticListenerSubscriber : IObserver<KeyValuePair<string, object?>>, IDisposable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 17 additions & 0 deletions src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,21 @@ internal static bool IsRecordingEnabled
=> Source.HasListeners()
? Source.StartActivity(operationName, activityKind)
: null;

/// <summary>
/// Starts a qyl span stamped to the ambient (framework) <see cref="Activity"/>'s real start time,
/// so DiagnosticListener bridges that only observe the completion (<c>*.Stop</c>) 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.
/// </summary>
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);
Comment on lines +40 to +43
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ private static class NetRuntimeMetrics
QylMetricNames.ProcessRuntimeDotnetGcObjectsSize,
static () => GC.GetTotalMemory(false),
"By");
private static readonly ObservableGauge<int> 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<int> ThreadPoolThreads = Meter.CreateObservableUpDownCounter(
QylMetricNames.ProcessRuntimeDotnetThreadPoolThreadsCount,
static () => ThreadPool.ThreadCount);
static () => ThreadPool.ThreadCount,
unit: "{thread}");
private static readonly ObservableGauge<long> ThreadPoolQueueLength = Meter.CreateObservableGauge(
QylMetricNames.ProcessRuntimeDotnetThreadPoolQueueLength,
static () => ThreadPool.PendingWorkItemCount);
Expand Down
Loading