diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityDateTimeHelper.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityDateTimeHelper.cs new file mode 100644 index 0000000000..63027679a4 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityDateTimeHelper.cs @@ -0,0 +1,82 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// https://github.com/dotnet/runtime/blob/75662173e3918f2176b74e467dc8e41d4f01d4d4/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.DateTime.netfx.cs + +// Adjusted to our coding standards and moved to separate class + +using System.Diagnostics; + +namespace OpenTelemetry.Instrumentation.AspNet; + +internal static class ActivityDateTimeHelper +{ +#pragma warning disable CA1823 // suppress unused field warning, as it's used to keep the timer alive + private static readonly Timer SyncTimeUpdater = InitializeSyncTimer(); +#pragma warning restore CA1823 + private static readonly double TickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency; + + private static TimeSync timeSync = new(); + + /// + /// Returns high resolution (1 DateTime tick) current UTC DateTime. + /// + /// High resolution UTC DateTime. + internal static DateTime GetUtcNow() + { + // DateTime.UtcNow accuracy on .NET Framework is ~16ms, this method + // uses combination of Stopwatch and DateTime to calculate accurate UtcNow. + + var tmp = timeSync; + + // Timer ticks need to be converted to DateTime ticks + long dateTimeTicksDiff = (long)((Stopwatch.GetTimestamp() - tmp.SyncStopwatchTicks) * TickFrequency); + + // DateTime.AddSeconds (or Milliseconds) rounds value to 1 ms, use AddTicks to prevent it + return tmp.SyncUtcNow.AddTicks(dateTimeTicksDiff); + } + + private static void Sync() + { + // wait for DateTime.UtcNow update to the next granular value + Thread.Sleep(1); + timeSync = new TimeSync(); + } + + [System.Security.SecuritySafeCritical] + private static Timer InitializeSyncTimer() + { + Timer timer; + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer causing them to live forever + bool restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + timer = new Timer(static _ => Sync(), null, 0, 7_200_000); // 2 hours + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + + return timer; + } + + private sealed class TimeSync + { + public readonly DateTime SyncUtcNow = DateTime.UtcNow; + public readonly long SyncStopwatchTicks = Stopwatch.GetTimestamp(); + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs index dddce54315..173135a544 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs @@ -120,7 +120,7 @@ public static void StopAspNetActivity(TextMapPropagator textMapPropagator, Activ // Note that the activity must not be stopped before the callback is called. if (aspNetActivity.Duration == TimeSpan.Zero) { - aspNetActivity.SetEndTime(DateTime.UtcNow); + aspNetActivity.SetEndTime(ActivityDateTimeHelper.GetUtcNow()); } try diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md index 09aee59426..6651d48593 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md @@ -11,6 +11,9 @@ responsible for providing function returning `Activity`. ([#3151](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3151)) +* Fixed an inaccurate span end of time. + ([#3171](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3171)) + ## 1.12.0-beta.2 Released 2025-Sep-18