|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers.Binary;
using System.Buffers.Text;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace System.Diagnostics
{
/// <summary>
/// Carries the <see cref="Activity.Current"/> changed event data.
/// </summary>
public readonly struct ActivityChangedEventArgs
{
internal ActivityChangedEventArgs(Activity? previous, Activity? current)
{
Previous = previous;
Current = current;
}
/// <summary>
/// Gets <see cref="Activity"/> object before the event.
/// </summary>
public Activity? Previous { get; init; }
/// <summary>
/// Gets <see cref="Activity"/> object after the event.
/// </summary>
public Activity? Current { get; init; }
}
/// <summary>
/// Activity represents operation with context to be used for logging.
/// Activity has operation name, Id, start time and duration, tags and baggage.
///
/// Current activity can be accessed with static AsyncLocal variable Activity.Current.
///
/// Activities should be created with constructor, configured as necessary
/// and then started with Activity.Start method which maintains parent-child
/// relationships for the activities and sets Activity.Current.
///
/// When activity is finished, it should be stopped with static Activity.Stop method.
///
/// No methods on Activity allow exceptions to escape as a response to bad inputs.
/// They are thrown and caught (that allows Debuggers and Monitors to see the error)
/// but the exception is suppressed, and the operation does something reasonable (typically
/// doing nothing).
/// </summary>
public partial class Activity : IDisposable
{
#pragma warning disable CA1825 // Array.Empty<T>() doesn't exist in all configurations
private static readonly IEnumerable<KeyValuePair<string, string?>> s_emptyBaggageTags = new KeyValuePair<string, string?>[0];
private static readonly IEnumerable<KeyValuePair<string, object?>> s_emptyTagObjects = new KeyValuePair<string, object?>[0];
private static readonly IEnumerable<ActivityLink> s_emptyLinks = new ActivityLink[0];
private static readonly IEnumerable<ActivityEvent> s_emptyEvents = new ActivityEvent[0];
#pragma warning restore CA1825
private static readonly ActivitySource s_defaultSource = new ActivitySource(string.Empty);
private static readonly AsyncLocal<Activity?> s_current = new AsyncLocal<Activity?>();
private const byte ActivityTraceFlagsIsSet = 0b_1_0000000; // Internal flag to indicate if flags have been set
private const int RequestIdMaxLength = 1024;
// Used to generate an ID it represents the machine and process we are in.
private static readonly string s_uniqSuffix = $"-{GetRandomNumber():x}.";
// A unique number inside the appdomain, randomized between appdomains.
// Int gives enough randomization and keeps hex-encoded s_currentRootId 8 chars long for most applications
private static long s_currentRootId = (uint)GetRandomNumber();
private static ActivityIdFormat s_defaultIdFormat;
/// <summary>
/// Event occur when the <see cref="Activity.Current"/> value changes.
/// </summary>
public static event EventHandler<ActivityChangedEventArgs>? CurrentChanged;
/// <summary>
/// Normally if the ParentID is defined, the format of that is used to determine the
/// format used by the Activity. However if ForceDefaultFormat is set to true, the
/// ID format will always be the DefaultIdFormat even if the ParentID is define and is
/// a different format.
/// </summary>
public static bool ForceDefaultIdFormat { get; set; }
private string? _traceState;
private State _state;
private int _currentChildId; // A unique number for all children of this activity.
// State associated with ID.
private string? _id;
private string? _rootId;
// State associated with ParentId.
private string? _parentId;
// W3C formats
private string? _parentSpanId;
private string? _traceId;
private string? _spanId;
private byte _w3CIdFlags;
private byte _parentTraceFlags;
private TagsLinkedList? _tags;
private BaggageLinkedList? _baggage;
private DiagLinkedList<ActivityLink>? _links;
private DiagLinkedList<ActivityEvent>? _events;
private Dictionary<string, object>? _customProperties;
private string? _displayName;
private ActivityStatusCode _statusCode;
private string? _statusDescription;
private Activity? _previousActiveActivity;
/// <summary>
/// Gets status code of the current activity object.
/// </summary>
public ActivityStatusCode Status => _statusCode;
/// <summary>
/// Gets the status description of the current activity object.
/// </summary>
public string? StatusDescription => _statusDescription;
/// <summary>
/// Gets whether the parent context was created from remote propagation.
/// </summary>
public bool HasRemoteParent { get; private set; }
/// <summary>
/// Gets or sets the current operation (Activity) for the current thread. This flows
/// across async calls.
/// </summary>
public static Activity? Current
{
get { return s_current.Value; }
set
{
if (ValidateSetCurrent(value))
{
SetCurrent(value);
}
}
}
/// <summary>
/// Sets the status code and description on the current activity object.
/// </summary>
/// <param name="code">The status code</param>
/// <param name="description">The error status description</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
/// <remarks>
/// When passing code value different than ActivityStatusCode.Error, the Activity.StatusDescription will reset to null value.
/// The description parameter will be respected only when passing ActivityStatusCode.Error value.
/// </remarks>
public Activity SetStatus(ActivityStatusCode code, string? description = null)
{
_statusCode = code;
_statusDescription = code == ActivityStatusCode.Error ? description : null;
return this;
}
/// <summary>
/// Gets the relationship between the Activity, its parents, and its children in a Trace.
/// </summary>
public ActivityKind Kind { get; private set; } = ActivityKind.Internal;
/// <summary>
/// An operation name is a COARSEST name that is useful grouping/filtering.
/// The name is typically a compile-time constant. Names of Rest APIs are
/// reasonable, but arguments (e.g. specific accounts etc), should not be in
/// the name but rather in the tags.
/// </summary>
public string OperationName { get; }
/// <summary>Gets or sets the display name of the Activity</summary>
/// <remarks>
/// DisplayName is intended to be used in a user interface and need not be the same as OperationName.
/// </remarks>
public string DisplayName
{
get => _displayName ?? OperationName;
set => _displayName = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>Get the ActivitySource object associated with this Activity.</summary>
/// <remarks>
/// All Activities created from public constructors will have a singleton source where the source name is an empty string.
/// Otherwise, the source will hold the object that created the Activity through ActivitySource.StartActivity.
/// </remarks>
public ActivitySource Source { get; private set; }
/// <summary>
/// If the Activity that created this activity is from the same process you can get
/// that Activity with Parent. However, this can be null if the Activity has no
/// parent (a root activity) or if the Parent is from outside the process.
/// </summary>
/// <seealso cref="ParentId"/>
public Activity? Parent { get; private set; }
/// <summary>
/// If the Activity has ended (<see cref="Stop"/> or <see cref="SetEndTime"/> was called) then this is the delta
/// between <see cref="StartTimeUtc"/> and end. If Activity is not ended and <see cref="SetEndTime"/> was not called then this is
/// <see cref="TimeSpan.Zero"/>.
/// </summary>
public TimeSpan Duration { get; private set; }
/// <summary>
/// The time that operation started. It will typically be initialized when <see cref="Start"/>
/// is called, but you can set at any time via <see cref="SetStartTime(DateTime)"/>.
/// </summary>
public DateTime StartTimeUtc { get; private set; }
/// <summary>
/// This is an ID that is specific to a particular request. Filtering
/// to a particular ID ensures that you get only one request that matches.
/// Id has a hierarchical structure: '|root-id.id1_id2.id3_' Id is generated when
/// <see cref="Start"/> is called by appending suffix to Parent.Id
/// or ParentId; Activity has no Id until it started
/// <para/>
/// See <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md#id-format"/> for more details
/// </summary>
/// <example>
/// Id looks like '|a000b421-5d183ab6.1.8e2d4c28_1.':<para />
/// - '|a000b421-5d183ab6.' - Id of the first, top-most, Activity created<para />
/// - '|a000b421-5d183ab6.1.' - Id of a child activity. It was started in the same process as the first activity and ends with '.'<para />
/// - '|a000b421-5d183ab6.1.8e2d4c28_' - Id of the grand child activity. It was started in another process and ends with '_'<para />
/// 'a000b421-5d183ab6' is a <see cref="RootId"/> for the first Activity and all its children
/// </example>
public string? Id
{
get
{
// if we represented it as a traceId-spanId, convert it to a string.
// We can do this concatenation with a stackalloced Span<char> if we actually used Id a lot.
if (_id == null && _spanId != null)
{
// Convert flags to binary.
Span<char> flagsChars = stackalloc char[2];
HexConverter.ToCharsBuffer((byte)((~ActivityTraceFlagsIsSet) & _w3CIdFlags), flagsChars, 0, HexConverter.Casing.Lower);
string id =
#if NET
string.Create(null, stackalloc char[128], $"00-{_traceId}-{_spanId}-{flagsChars}");
#else
"00-" + _traceId + "-" + _spanId + "-" + flagsChars.ToString();
#endif
Interlocked.CompareExchange(ref _id, id, null);
}
return _id;
}
}
/// <summary>
/// If the parent for this activity comes from outside the process, the activity
/// does not have a Parent Activity but MAY have a ParentId (which was deserialized from
/// from the parent). This accessor fetches the parent ID if it exists at all.
/// Note this can be null if this is a root Activity (it has no parent)
/// <para/>
/// See <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md#id-format"/> for more details
/// </summary>
public string? ParentId
{
get
{
// if we represented it as a traceId-spanId, convert it to a string.
if (_parentId == null)
{
if (_parentSpanId != null)
{
Span<char> flagsChars = stackalloc char[2];
HexConverter.ToCharsBuffer((byte)((~ActivityTraceFlagsIsSet) & _parentTraceFlags), flagsChars, 0, HexConverter.Casing.Lower);
string parentId =
#if NET
string.Create(null, stackalloc char[128], $"00-{_traceId}-{_parentSpanId}-{flagsChars}");
#else
"00-" + _traceId + "-" + _parentSpanId + "-" + flagsChars.ToString();
#endif
Interlocked.CompareExchange(ref _parentId, parentId, null);
}
else if (Parent != null)
{
Interlocked.CompareExchange(ref _parentId, Parent.Id, null);
}
}
return _parentId;
}
}
/// <summary>
/// Root Id is substring from Activity.Id (or ParentId) between '|' (or beginning) and first '.'.
/// Filtering by root Id allows to find all Activities involved in operation processing.
/// RootId may be null if Activity has neither ParentId nor Id.
/// See <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md#id-format"/> for more details
/// </summary>
public string? RootId
{
get
{
//we expect RootId to be requested at any time after activity is created,
//possibly even before it was started for sampling or logging purposes
//Presumably, it will be called by logging systems for every log record, so we cache it.
if (_rootId == null)
{
string? rootId = null;
if (Id != null)
{
rootId = GetRootId(Id);
}
else if (ParentId != null)
{
rootId = GetRootId(ParentId);
}
if (rootId != null)
{
Interlocked.CompareExchange(ref _rootId, rootId, null);
}
}
return _rootId;
}
}
/// <summary>
/// Tags are string-string key-value pairs that represent information that will
/// be logged along with the Activity to the logging system. This information
/// however is NOT passed on to the children of this activity.
/// </summary>
/// <seealso cref="Baggage"/>
public IEnumerable<KeyValuePair<string, string?>> Tags
{
get => _tags?.EnumerateStringValues() ?? s_emptyBaggageTags;
}
/// <summary>
/// List of the tags which represent information that will be logged along with the Activity to the logging system.
/// This information however is NOT passed on to the children of this activity.
/// </summary>
public IEnumerable<KeyValuePair<string, object?>> TagObjects
{
get => _tags ?? s_emptyTagObjects;
}
/// <summary>
/// Events is the list of all <see cref="ActivityEvent" /> objects attached to this Activity object.
/// If there is not any <see cref="ActivityEvent" /> object attached to the Activity object, Events will return empty list.
/// </summary>
public IEnumerable<ActivityEvent> Events
{
get => _events ?? s_emptyEvents;
}
/// <summary>
/// Links is the list of all <see cref="ActivityLink" /> objects attached to this Activity object.
/// If there is no any <see cref="ActivityLink" /> object attached to the Activity object, Links will return empty list.
/// </summary>
public IEnumerable<ActivityLink> Links
{
get => _links ?? s_emptyLinks;
}
/// <summary>
/// Baggage is string-string key-value pairs that represent information that will
/// be passed along to children of this activity. Baggage is serialized
/// when requests leave the process (along with the ID). Typically Baggage is
/// used to do fine-grained control over logging of the activity and any children.
/// In general, if you are not using the data at runtime, you should be using Tags
/// instead.
/// </summary>
public IEnumerable<KeyValuePair<string, string?>> Baggage
{
get
{
for (Activity? activity = this; activity != null; activity = activity.Parent)
{
if (activity._baggage != null)
{
return Iterate(activity);
}
}
return s_emptyBaggageTags;
static IEnumerable<KeyValuePair<string, string?>> Iterate(Activity? activity)
{
Debug.Assert(activity != null);
do
{
if (activity._baggage != null)
{
for (DiagNode<KeyValuePair<string, string?>>? current = activity._baggage.First; current != null; current = current.Next)
{
yield return current.Value;
}
}
activity = activity.Parent;
} while (activity != null);
}
}
}
/// <summary>
/// Enumerate the tags attached to this Activity object.
/// </summary>
/// <returns><see cref="Enumerator{T}"/>.</returns>
public Enumerator<KeyValuePair<string, object?>> EnumerateTagObjects() => new Enumerator<KeyValuePair<string, object?>>(_tags?.First);
/// <summary>
/// Enumerate the <see cref="ActivityEvent" /> objects attached to this Activity object.
/// </summary>
/// <returns><see cref="Enumerator{T}"/>.</returns>
public Enumerator<ActivityEvent> EnumerateEvents() => new Enumerator<ActivityEvent>(_events?.First);
/// <summary>
/// Enumerate the <see cref="ActivityLink" /> objects attached to this Activity object.
/// </summary>
/// <returns><see cref="Enumerator{T}"/>.</returns>
public Enumerator<ActivityLink> EnumerateLinks() => new Enumerator<ActivityLink>(_links?.First);
/// <summary>
/// Returns the value of the key-value pair added to the activity with <see cref="AddBaggage(string, string)"/>.
/// Returns null if that key does not exist.
/// </summary>
public string? GetBaggageItem(string key)
{
foreach (KeyValuePair<string, string?> keyValue in Baggage)
if (key == keyValue.Key)
return keyValue.Value;
return null;
}
/// <summary>
/// Returns the value of the Activity tag mapped to the input key/>.
/// Returns null if that key does not exist.
/// </summary>
/// <param name="key">The tag key string.</param>
/// <returns>The tag value mapped to the input key.</returns>
public object? GetTagItem(string key) => _tags?.Get(key) ?? null;
/* Constructors Builder methods */
/// <summary>
/// Note that Activity has a 'builder' pattern, where you call the constructor, a number of 'Set*' and 'Add*' APIs and then
/// call <see cref="Start"/> to build the activity. You MUST call <see cref="Start"/> before using it.
/// </summary>
/// <param name="operationName">Operation's name <see cref="OperationName"/></param>
public Activity(string operationName)
{
Source = s_defaultSource;
// Allow data by default in the constructor to keep the compatibility.
IsAllDataRequested = true;
if (string.IsNullOrEmpty(operationName))
{
NotifyError(new ArgumentException(SR.OperationNameInvalid));
}
OperationName = operationName ?? string.Empty;
}
/// <summary>
/// Update the Activity to have a tag with an additional 'key' and value 'value'.
/// This shows up in the <see cref="Tags"/> enumeration. It is meant for information that
/// is useful to log but not needed for runtime control (for the latter, <see cref="Baggage"/>)
/// </summary>
/// <returns><see langword="this" /> for convenient chaining.</returns>
/// <param name="key">The tag key name</param>
/// <param name="value">The tag value mapped to the input key</param>
public Activity AddTag(string key, string? value) => AddTag(key, (object?)value);
/// <summary>
/// Update the Activity to have a tag with an additional 'key' and value 'value'.
/// This shows up in the <see cref="TagObjects"/> enumeration. It is meant for information that
/// is useful to log but not needed for runtime control (for the latter, <see cref="Baggage"/>)
/// </summary>
/// <returns><see langword="this" /> for convenient chaining.</returns>
/// <param name="key">The tag key name</param>
/// <param name="value">The tag value mapped to the input key</param>
public Activity AddTag(string key, object? value)
{
KeyValuePair<string, object?> kvp = new KeyValuePair<string, object?>(key, value);
if (_tags != null || Interlocked.CompareExchange(ref _tags, new TagsLinkedList(kvp), null) != null)
{
_tags.Add(kvp);
}
return this;
}
/// <summary>
/// Add or update the Activity tag with the input key and value.
/// If the input value is null
/// - if the collection has any tag with the same key, then this tag will get removed from the collection.
/// - otherwise, nothing will happen and the collection will not change.
/// If the input value is not null
/// - if the collection has any tag with the same key, then the value mapped to this key will get updated with the new input value.
/// - otherwise, the key and value will get added as a new tag to the collection.
/// </summary>
/// <param name="key">The tag key name</param>
/// <param name="value">The tag value mapped to the input key</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
public Activity SetTag(string key, object? value)
{
KeyValuePair<string, object?> kvp = new KeyValuePair<string, object?>(key, value);
if (_tags != null || Interlocked.CompareExchange(ref _tags, new TagsLinkedList(kvp, set: true), null) != null)
{
_tags.Set(kvp);
}
return this;
}
/// <summary>
/// Add <see cref="ActivityEvent" /> object to the <see cref="Events" /> list.
/// </summary>
/// <param name="e"> object of <see cref="ActivityEvent"/> to add to the attached events list.</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
public Activity AddEvent(ActivityEvent e)
{
if (_events != null || Interlocked.CompareExchange(ref _events, new DiagLinkedList<ActivityEvent>(e), null) != null)
{
_events.Add(e);
}
return this;
}
/// <summary>
/// Add an <see cref="ActivityEvent" /> object containing the exception information to the <see cref="Events" /> list.
/// </summary>
/// <param name="exception">The exception to add to the attached events list.</param>
/// <param name="tags">The tags to add to the exception event.</param>
/// <param name="timestamp">The timestamp to add to the exception event.</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
/// <remarks>
/// <para>- The name of the event will be "exception", and it will include the tags "exception.message", "exception.stacktrace", and "exception.type",
/// in addition to the tags provided in the <paramref name="tags"/> parameter.</para>
/// <para>- Any registered <see cref="ActivityListener"/> with the <see cref="ActivityListener.ExceptionRecorder"/> callback will be notified about this exception addition
/// before the <see cref="ActivityEvent" /> object is added to the <see cref="Events" /> list.</para>
/// <para>- Any registered <see cref="ActivityListener"/> with the <see cref="ActivityListener.ExceptionRecorder"/> callback that adds "exception.message", "exception.stacktrace", or "exception.type" tags
/// will not have these tags overwritten, except by any subsequent <see cref="ActivityListener"/> that explicitly overwrites them.</para>
/// </remarks>
public Activity AddException(Exception exception, in TagList tags = default, DateTimeOffset timestamp = default)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
TagList exceptionTags = tags;
Source.NotifyActivityAddException(this, exception, ref exceptionTags);
const string ExceptionEventName = "exception";
const string ExceptionMessageTag = "exception.message";
const string ExceptionStackTraceTag = "exception.stacktrace";
const string ExceptionTypeTag = "exception.type";
bool hasMessage = false;
bool hasStackTrace = false;
bool hasType = false;
for (int i = 0; i < exceptionTags.Count; i++)
{
if (exceptionTags[i].Key == ExceptionMessageTag)
{
hasMessage = true;
}
else if (exceptionTags[i].Key == ExceptionStackTraceTag)
{
hasStackTrace = true;
}
else if (exceptionTags[i].Key == ExceptionTypeTag)
{
hasType = true;
}
}
if (!hasMessage)
{
exceptionTags.Add(new KeyValuePair<string, object?>(ExceptionMessageTag, exception.Message));
}
if (!hasStackTrace)
{
exceptionTags.Add(new KeyValuePair<string, object?>(ExceptionStackTraceTag, exception.ToString()));
}
if (!hasType)
{
exceptionTags.Add(new KeyValuePair<string, object?>(ExceptionTypeTag, exception.GetType().ToString()));
}
return AddEvent(new ActivityEvent(ExceptionEventName, timestamp, ref exceptionTags));
}
/// <summary>
/// Add an <see cref="ActivityLink"/> to the <see cref="Links"/> list.
/// </summary>
/// <param name="link">The <see cref="ActivityLink"/> to add.</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
/// <remarks>
/// For contexts that are available during span creation, adding links at span creation is preferred to calling <see cref="AddLink(ActivityLink)" /> later,
/// because head sampling decisions can only consider information present during span creation.
/// </remarks>
public Activity AddLink(ActivityLink link)
{
if (_links != null || Interlocked.CompareExchange(ref _links, new DiagLinkedList<ActivityLink>(link), null) != null)
{
_links.Add(link);
}
return this;
}
/// <summary>
/// Update the Activity to have baggage with an additional 'key' and value 'value'.
/// This shows up in the <see cref="Baggage"/> enumeration as well as the <see cref="GetBaggageItem(string)"/>
/// method.
/// Baggage is meant for information that is needed for runtime control. For information
/// that is simply useful to show up in the log with the activity use <see cref="Tags"/>.
/// Returns 'this' for convenient chaining.
/// </summary>
/// <returns><see langword="this" /> for convenient chaining.</returns>
public Activity AddBaggage(string key, string? value)
{
KeyValuePair<string, string?> kvp = new KeyValuePair<string, string?>(key, value);
if (_baggage != null || Interlocked.CompareExchange(ref _baggage, new BaggageLinkedList(kvp), null) != null)
{
_baggage.Add(kvp);
}
return this;
}
/// <summary>
/// Add or update the Activity baggage with the input key and value.
/// If the input value is null
/// - if the collection has any baggage with the same key, then this baggage will get removed from the collection.
/// - otherwise, nothing will happen and the collection will not change.
/// If the input value is not null
/// - if the collection has any baggage with the same key, then the value mapped to this key will get updated with the new input value.
/// - otherwise, the key and value will get added as a new baggage to the collection.
/// </summary>
/// <param name="key">The baggage key name</param>
/// <param name="value">The baggage value mapped to the input key</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
public Activity SetBaggage(string key, string? value)
{
KeyValuePair<string, string?> kvp = new KeyValuePair<string, string?>(key, value);
if (_baggage != null || Interlocked.CompareExchange(ref _baggage, new BaggageLinkedList(kvp, set: true), null) != null)
{
_baggage.Set(kvp);
}
return this;
}
/// <summary>
/// Updates the Activity To indicate that the activity with ID <paramref name="parentId"/>
/// caused this activity. This is intended to be used only at 'boundary'
/// scenarios where an activity from another process logically started
/// this activity. The Parent ID shows up the Tags (as well as the ParentID
/// property), and can be used to reconstruct the causal tree.
/// Returns 'this' for convenient chaining.
/// </summary>
/// <param name="parentId">The id of the parent operation.</param>
public Activity SetParentId(string parentId)
{
if (_id != null || _spanId != null)
{
// Cannot set the parent on already started Activity.
NotifyError(new InvalidOperationException(SR.ActivitySetParentAlreadyStarted));
}
else if (Parent != null)
{
NotifyError(new InvalidOperationException(SR.SetParentIdOnActivityWithParent));
}
else if (ParentId != null || _parentSpanId != null)
{
NotifyError(new InvalidOperationException(SR.ParentIdAlreadySet));
}
else if (string.IsNullOrEmpty(parentId))
{
NotifyError(new ArgumentException(SR.ParentIdInvalid));
}
else
{
_parentId = parentId;
}
return this;
}
/// <summary>
/// Set the parent ID using the W3C convention using a TraceId and a SpanId. This
/// constructor has the advantage that no string manipulation is needed to set the ID.
/// </summary>
public Activity SetParentId(ActivityTraceId traceId, ActivitySpanId spanId, ActivityTraceFlags activityTraceFlags = ActivityTraceFlags.None)
{
if (_id != null || _spanId != null)
{
// Cannot set the parent on already started Activity.
NotifyError(new InvalidOperationException(SR.ActivitySetParentAlreadyStarted));
}
else if (Parent != null)
{
NotifyError(new InvalidOperationException(SR.SetParentIdOnActivityWithParent));
}
else if (ParentId != null || _parentSpanId != null)
{
NotifyError(new InvalidOperationException(SR.ParentIdAlreadySet));
}
else
{
_traceId = traceId.ToHexString(); // The child will share the parent's traceId.
_parentSpanId = spanId.ToHexString();
ActivityTraceFlags = activityTraceFlags;
_parentTraceFlags = (byte)activityTraceFlags;
}
return this;
}
/// <summary>
/// Update the Activity to set start time
/// </summary>
/// <param name="startTimeUtc">Activity start time in UTC (Greenwich Mean Time)</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
public Activity SetStartTime(DateTime startTimeUtc)
{
if (startTimeUtc.Kind != DateTimeKind.Utc)
{
NotifyError(new InvalidOperationException(SR.StartTimeNotUtc));
}
else
{
StartTimeUtc = startTimeUtc;
}
return this;
}
/// <summary>
/// Update the Activity to set <see cref="Duration"/>
/// as a difference between <see cref="StartTimeUtc"/>
/// and <paramref name="endTimeUtc"/>.
/// </summary>
/// <param name="endTimeUtc">Activity stop time in UTC (Greenwich Mean Time)</param>
/// <returns><see langword="this" /> for convenient chaining.</returns>
public Activity SetEndTime(DateTime endTimeUtc)
{
if (endTimeUtc.Kind != DateTimeKind.Utc)
{
NotifyError(new InvalidOperationException(SR.EndTimeNotUtc));
}
else
{
Duration = endTimeUtc - StartTimeUtc;
if (Duration.Ticks <= 0)
Duration = new TimeSpan(1); // We want Duration of 0 to mean 'EndTime not set)
}
return this;
}
/// <summary>
/// Get the context of the activity. Context becomes valid only if the activity has been started.
/// otherwise will default context.
/// </summary>
public ActivityContext Context => new ActivityContext(TraceId, SpanId, ActivityTraceFlags, TraceStateString);
/// <summary>
/// Starts activity
/// <list type="bullet">
/// <item>Sets <see cref="Parent"/> to hold <see cref="Current"/>.</item>
/// <item>Sets <see cref="Current"/> to this activity.</item>
/// <item>If <see cref="StartTimeUtc"/> was not set previously, sets it to <see cref="DateTime.UtcNow"/>.</item>
/// <item>Generates a unique <see cref="Id"/> for this activity.</item>
/// </list>
/// Use <see cref="DiagnosticSource.StartActivity(Activity, object)"/> to start activity and write start event.
/// </summary>
/// <seealso cref="DiagnosticSource.StartActivity(Activity, object)"/>
/// <seealso cref="SetStartTime(DateTime)"/>
public Activity Start()
{
// Has the ID already been set (have we called Start()).
if (_id != null || _spanId != null)
{
NotifyError(new InvalidOperationException(SR.ActivityStartAlreadyStarted));
}
else
{
_previousActiveActivity = Current;
if (_parentId == null && _parentSpanId is null)
{
if (_previousActiveActivity != null)
{
// The parent change should not form a loop. We are actually guaranteed this because
// 1. Un-started activities can't be 'Current' (thus can't be 'parent'), we throw if you try.
// 2. All started activities have a finite parent change (by inductive reasoning).
Parent = _previousActiveActivity;
}
}
if (StartTimeUtc == default)
StartTimeUtc = GetUtcNow();
if (IdFormat == ActivityIdFormat.Unknown)
{
// Figure out what format to use.
IdFormat =
ForceDefaultIdFormat ? DefaultIdFormat :
Parent != null ? Parent.IdFormat :
_parentSpanId != null ? ActivityIdFormat.W3C :
_parentId == null ? DefaultIdFormat :
IsW3CId(_parentId) ? ActivityIdFormat.W3C :
ActivityIdFormat.Hierarchical;
}
// Generate the ID in the appropriate format.
if (IdFormat == ActivityIdFormat.W3C)
GenerateW3CId();
else
_id = GenerateHierarchicalId();
SetCurrent(this);
Source.NotifyActivityStart(this);
}
return this;
}
/// <summary>
/// Stops activity: sets <see cref="Current"/> to <see cref="Parent"/>.
/// If end time was not set previously, sets <see cref="Duration"/> as a difference between <see cref="DateTime.UtcNow"/> and <see cref="StartTimeUtc"/>
/// Use <see cref="DiagnosticSource.StopActivity(Activity, object)"/> to stop activity and write stop event.
/// </summary>
/// <seealso cref="DiagnosticSource.StopActivity(Activity, object)"/>
/// <seealso cref="SetEndTime(DateTime)"/>
public void Stop()
{
if (_id == null && _spanId == null)
{
NotifyError(new InvalidOperationException(SR.ActivityNotStarted));
return;
}
if (!IsStopped)
{
IsStopped = true;
if (Duration == TimeSpan.Zero)
{
SetEndTime(GetUtcNow());
}
Source.NotifyActivityStop(this);
SetCurrent(_previousActiveActivity);
}
}
/* W3C support functionality (see https://w3c.github.io/trace-context) */
/// <summary>
/// Holds the W3C 'tracestate' header as a string.
///
/// Tracestate is intended to carry information supplemental to trace identity contained
/// in traceparent. List of key value pairs carried by tracestate convey information
/// about request position in multiple distributed tracing graphs. It is typically used
/// by distributed tracing systems and should not be used as a general purpose baggage
/// as this use may break correlation of a distributed trace.
///
/// Logically it is just a kind of baggage (if flows just like baggage), but because
/// it is expected to be special cased (it has its own HTTP header), it is more
/// convenient/efficient if it is not lumped in with other baggage.
/// </summary>
public string? TraceStateString
{
get
{
for (Activity? activity = this; activity != null; activity = activity.Parent)
{
string? val = activity._traceState;
if (val != null)
return val;
}
return null;
}
set
{
_traceState = value;
}
}
/// <summary>
/// If the Activity has the W3C format, this returns the ID for the SPAN part of the Id.
/// Otherwise it returns a zero SpanId.
/// </summary>
public ActivitySpanId SpanId
{
get
{
if (_spanId is null)
{
if (_id != null && IdFormat == ActivityIdFormat.W3C)
{
ActivitySpanId activitySpanId = ActivitySpanId.CreateFromString(_id.AsSpan(36, 16));
string spanId = activitySpanId.ToHexString();
Interlocked.CompareExchange(ref _spanId, spanId, null);
}
}
return new ActivitySpanId(_spanId);
}
}
/// <summary>
/// If the Activity has the W3C format, this returns the ID for the TraceId part of the Id.
/// Otherwise it returns a zero TraceId.
/// </summary>
public ActivityTraceId TraceId
{
get
{
if (_traceId is null)
{
TrySetTraceIdFromParent();
}
return new ActivityTraceId(_traceId);
}
}
/// <summary>
/// True if the W3CIdFlags.Recorded flag is set.
/// </summary>
public bool Recorded { get => (ActivityTraceFlags & ActivityTraceFlags.Recorded) != 0; }
/// <summary>
/// Indicate if the this Activity object should be populated with all the propagation info and also all other
/// properties such as Links, Tags, and Events.
/// </summary>
public bool IsAllDataRequested { get; set; }
/// <summary>
/// Return the flags (defined by the W3C ID specification) associated with the activity.
/// </summary>
public ActivityTraceFlags ActivityTraceFlags
{
get
{
if (!W3CIdFlagsSet)
{
TrySetTraceFlagsFromParent();
}
return (ActivityTraceFlags)((~ActivityTraceFlagsIsSet) & _w3CIdFlags);
}
set
{
_w3CIdFlags = (byte)(ActivityTraceFlagsIsSet | (byte)value);
}
}
/// <summary>
/// If the parent Activity ID has the W3C format, this returns the ID for the SpanId part of the ParentId.
/// Otherwise it returns a zero SpanId.
/// </summary>
public ActivitySpanId ParentSpanId
{
get
{
if (_parentSpanId is null)
{
string? parentSpanId = null;
if (_parentId != null && IsW3CId(_parentId))
{
try
{
parentSpanId = ActivitySpanId.CreateFromString(_parentId.AsSpan(36, 16)).ToHexString();
}
catch { }
}
else if (Parent != null && Parent.IdFormat == ActivityIdFormat.W3C)
{
parentSpanId = Parent.SpanId.ToHexString();
}
if (parentSpanId != null)
{
Interlocked.CompareExchange(ref _parentSpanId, parentSpanId, null);
}
}
return new ActivitySpanId(_parentSpanId);
}
}
/// <summary>
/// When starting an Activity which does not have a parent context, the Trace Id will automatically be generated using random numbers.
/// TraceIdGenerator can be used to override the runtime's default Trace Id generation algorithm.
/// </summary>
/// <remarks>
/// - TraceIdGenerator needs to be set only if the default Trace Id generation is not enough for the app scenario.
/// - When setting TraceIdGenerator, ensure it is performant enough to avoid any slowness in the Activity starting operation.
/// - If TraceIdGenerator is set multiple times, the last set will be the one used for the Trace Id generation.
/// - Setting TraceIdGenerator to null will re-enable the default Trace Id generation algorithm.
/// </remarks>
public static Func<ActivityTraceId>? TraceIdGenerator { get; set; }
/* static state (configuration) */
/// <summary>
/// Activity tries to use the same format for IDs as its parent.
/// However if the activity has no parent, it has to do something.
/// This determines the default format we use.
/// </summary>
public static ActivityIdFormat DefaultIdFormat
{
get
{
if (s_defaultIdFormat == ActivityIdFormat.Unknown)
{
#if W3C_DEFAULT_ID_FORMAT
s_defaultIdFormat = LocalAppContextSwitches.DefaultActivityIdFormatIsHierarchial ? ActivityIdFormat.Hierarchical : ActivityIdFormat.W3C;
#else
s_defaultIdFormat = ActivityIdFormat.Hierarchical;
#endif // W3C_DEFAULT_ID_FORMAT
}
return s_defaultIdFormat;
}
set
{
if (!(ActivityIdFormat.Hierarchical <= value && value <= ActivityIdFormat.W3C))
throw new ArgumentException(SR.ActivityIdFormatInvalid);
s_defaultIdFormat = value;
}
}
/// <summary>
/// Sets IdFormat on the Activity before it is started. It takes precedence over
/// Parent.IdFormat, ParentId format, DefaultIdFormat and ForceDefaultIdFormat.
/// </summary>
public Activity SetIdFormat(ActivityIdFormat format)
{
if (_id != null || _spanId != null)
{
NotifyError(new InvalidOperationException(SR.SetFormatOnStartedActivity));
}
else
{
IdFormat = format;
}
return this;
}
/// <summary>
/// Returns true if 'id' has the format of a WC3 id see https://w3c.github.io/trace-context
/// </summary>
private static bool IsW3CId(string id)
{
// A W3CId is
// * 2 hex chars Version (ff is invalid)
// * 1 char - char
// * 32 hex chars traceId
// * 1 char - char
// * 16 hex chars spanId
// * 1 char - char
// * 2 hex chars flags
// = 55 chars (see https://w3c.github.io/trace-context)
// The version (00-fe) is used to indicate that this is a WC3 ID.
return id.Length == 55 &&
('0' <= id[0] && id[0] <= '9' || 'a' <= id[0] && id[0] <= 'f') &&
('0' <= id[1] && id[1] <= '9' || 'a' <= id[1] && id[1] <= 'f') &&
(id[0] != 'f' || id[1] != 'f');
}
internal static bool TryConvertIdToContext(string traceParent, string? traceState, bool isRemote, out ActivityContext context)
{
context = default;
if (!IsW3CId(traceParent))
{
return false;
}
ReadOnlySpan<char> traceIdSpan = traceParent.AsSpan(3, 32);
ReadOnlySpan<char> spanIdSpan = traceParent.AsSpan(36, 16);
if (!ActivityTraceId.IsLowerCaseHexAndNotAllZeros(traceIdSpan) || !ActivityTraceId.IsLowerCaseHexAndNotAllZeros(spanIdSpan) ||
!HexConverter.IsHexLowerChar(traceParent[53]) || !HexConverter.IsHexLowerChar(traceParent[54]))
{
return false;
}
context = new ActivityContext(
new ActivityTraceId(traceIdSpan.ToString()),
new ActivitySpanId(spanIdSpan.ToString()),
(ActivityTraceFlags)ActivityTraceId.HexByteFromChars(traceParent[53], traceParent[54]),
traceState,
isRemote);
return true;
}
/// <summary>
/// Dispose will stop the Activity if it is already started and notify any event listeners. Nothing will happen otherwise.
/// </summary>
public void Dispose()
{
if (!IsStopped)
{
Stop();
}
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
}
/// <summary>
/// SetCustomProperty allow attaching any custom object to this Activity object.
/// If the property name was previously associated with other object, SetCustomProperty will update to use the new propert value instead.
/// </summary>
/// <param name="propertyName"> The name to associate the value with.<see cref="OperationName"/></param>
/// <param name="propertyValue">The object to attach and map to the property name.</param>
public void SetCustomProperty(string propertyName, object? propertyValue)
{
if (_customProperties == null)
{
Interlocked.CompareExchange(ref _customProperties, new Dictionary<string, object>(), null);
}
lock (_customProperties)
{
if (propertyValue == null)
{
_customProperties.Remove(propertyName);
}
else
{
_customProperties[propertyName] = propertyValue!;
}
}
}
/// <summary>
/// GetCustomProperty retrieve previously attached object mapped to the property name.
/// </summary>
/// <param name="propertyName"> The name to get the associated object with.</param>
/// <returns>The object mapped to the property name. Or null if there is no mapping previously done with this property name.</returns>
public object? GetCustomProperty(string propertyName)
{
// We don't check null name here as the dictionary is performing this check anyway.
if (_customProperties == null)
{
return null;
}
object? ret;
lock (_customProperties)
{
ret = _customProperties.TryGetValue(propertyName, out object? o) ? o! : null;
}
return ret;
}
internal static Activity Create(ActivitySource source, string name, ActivityKind kind, string? parentId, ActivityContext parentContext,
IEnumerable<KeyValuePair<string, object?>>? tags, IEnumerable<ActivityLink>? links, DateTimeOffset startTime,
ActivityTagsCollection? samplerTags, ActivitySamplingResult request, bool startIt, ActivityIdFormat idFormat, string? traceState)
{
Activity activity = new Activity(name);
activity.Source = source;
activity.Kind = kind;
activity.IdFormat = idFormat;
activity._traceState = traceState;
if (links != null)
{
using (IEnumerator<ActivityLink> enumerator = links.GetEnumerator())
{
if (enumerator.MoveNext())
{
activity._links = new DiagLinkedList<ActivityLink>(enumerator);
}
}
}
if (tags != null)
{
using (IEnumerator<KeyValuePair<string, object?>> enumerator = tags.GetEnumerator())
{
if (enumerator.MoveNext())
{
activity._tags = new TagsLinkedList(enumerator);
}
}
}
if (samplerTags != null)
{
if (activity._tags == null)
{
activity._tags = new TagsLinkedList(samplerTags!);
}
else
{
activity._tags.Add(samplerTags!);
}
}
if (parentId != null)
{
activity._parentId = parentId;
}
else if (parentContext != default)
{
activity._traceId = parentContext.TraceId.ToString();
if (parentContext.SpanId != default)
{
activity._parentSpanId = parentContext.SpanId.ToString();
}
activity.ActivityTraceFlags = parentContext.TraceFlags;
activity._parentTraceFlags = (byte)parentContext.TraceFlags;
activity.HasRemoteParent = parentContext.IsRemote;
}
activity.IsAllDataRequested = request == ActivitySamplingResult.AllData || request == ActivitySamplingResult.AllDataAndRecorded;
if (request == ActivitySamplingResult.AllDataAndRecorded)
{
activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded;
}
if (startTime != default)
{
activity.StartTimeUtc = startTime.UtcDateTime;
}
if (startIt)
{
activity.Start();
}
return activity;
}
private static void SetCurrent(Activity? activity)
{
EventHandler<ActivityChangedEventArgs>? handler = CurrentChanged;
if (handler is null)
{
s_current.Value = activity;
}
else
{
Activity? previous = s_current.Value;
s_current.Value = activity;
handler.Invoke(null, new ActivityChangedEventArgs(previous, activity));
}
}
/// <summary>
/// Set the ID (lazily, avoiding strings if possible) to a W3C ID (using the
/// traceId from the parent if possible
/// </summary>
private void GenerateW3CId()
{
// Called from .Start()
// Get the TraceId from the parent or make a new one.
if (_traceId is null)
{
if (!TrySetTraceIdFromParent())
{
Func<ActivityTraceId>? traceIdGenerator = TraceIdGenerator;
ActivityTraceId id = traceIdGenerator == null ? ActivityTraceId.CreateRandom() : traceIdGenerator();
_traceId = id.ToHexString();
}
}
if (!W3CIdFlagsSet)
{
TrySetTraceFlagsFromParent();
}
// Create a new SpanID.
_spanId = ActivitySpanId.CreateRandom().ToHexString();
}
private static void NotifyError(Exception exception)
{
// Throw and catch the exception. This lets it be seen by the debugger
// ETW, and other monitoring tools. However we immediately swallow the
// exception. We may wish in the future to allow users to hook this
// in other useful ways but for now we simply swallow the exceptions.
try
{
throw exception;
}
catch { }
}
/// <summary>
/// Returns a new ID using the Hierarchical Id
/// </summary>
private string GenerateHierarchicalId()
{
// Called from .Start()
string ret;
if (Parent != null)
{
// Normal start within the process
Debug.Assert(!string.IsNullOrEmpty(Parent.Id));
ret = AppendSuffix(Parent.Id, Interlocked.Increment(ref Parent._currentChildId).ToString(), '.');
}
else if (ParentId != null)
{
// Start from outside the process (e.g. incoming HTTP)
Debug.Assert(ParentId.Length != 0);
//sanitize external RequestId as it may not be hierarchical.
//we cannot update ParentId, we must let it be logged exactly as it was passed.
string parentId = ParentId[0] == '|' ? ParentId : '|' + ParentId;
char lastChar = parentId[parentId.Length - 1];
if (lastChar != '.' && lastChar != '_')
{
parentId += '.';
}
ret = AppendSuffix(parentId, Interlocked.Increment(ref s_currentRootId).ToString("x"), '_');
}
else
{
// A Root Activity (no parent).
ret = GenerateRootId();
}
// Useful place to place a conditional breakpoint.
return ret;
}
private string GetRootId(string id)
{
// If this is a W3C ID it has the format Version2-TraceId32-SpanId16-Flags2
// and the root ID is the TraceId.
if (IdFormat == ActivityIdFormat.W3C)
return id.Substring(3, 32);
//id MAY start with '|' and contain '.'. We return substring between them
//ParentId MAY NOT have hierarchical structure and we don't know if initially rootId was started with '|',
//so we must NOT include first '|' to allow mixed hierarchical and non-hierarchical request id scenarios
int rootEnd = id.IndexOf('.');
if (rootEnd < 0)
rootEnd = id.Length;
int rootStart = id[0] == '|' ? 1 : 0;
return id.Substring(rootStart, rootEnd - rootStart);
}
#pragma warning disable CA1822
private string AppendSuffix(string parentId, string suffix, char delimiter)
{
#if DEBUG
suffix = OperationName.Replace('.', '-') + "-" + suffix;
#endif
if (parentId.Length + suffix.Length < RequestIdMaxLength)
return parentId + suffix + delimiter;
//Id overflow:
//find position in RequestId to trim
int trimPosition = RequestIdMaxLength - 9; // overflow suffix + delimiter length is 9
while (trimPosition > 1)
{
if (parentId[trimPosition - 1] == '.' || parentId[trimPosition - 1] == '_')
break;
trimPosition--;
}
//ParentId is not valid Request-Id, let's generate proper one.
if (trimPosition == 1)
return GenerateRootId();
//generate overflow suffix
string overflowSuffix = ((int)GetRandomNumber()).ToString("x8");
return parentId.Substring(0, trimPosition) + overflowSuffix + '#';
}
#pragma warning restore CA1822
private static unsafe long GetRandomNumber()
{
// Use the first 8 bytes of the GUID as a random number.
Guid g = Guid.NewGuid();
return *((long*)&g);
}
private static bool ValidateSetCurrent(Activity? activity)
{
bool canSet = activity == null || (activity.Id != null && !activity.IsStopped);
if (!canSet)
{
NotifyError(new InvalidOperationException(SR.ActivityNotRunning));
}
return canSet;
}
private bool TrySetTraceIdFromParent()
{
Debug.Assert(_traceId is null);
if (Parent != null && Parent.IdFormat == ActivityIdFormat.W3C)
{
_traceId = Parent.TraceId.ToHexString();
}
else if (_parentId != null && IsW3CId(_parentId))
{
try
{
_traceId = ActivityTraceId.CreateFromString(_parentId.AsSpan(3, 32)).ToHexString();
}
catch
{
}
}
return _traceId != null;
}
private void TrySetTraceFlagsFromParent()
{
Debug.Assert(!W3CIdFlagsSet);
if (!W3CIdFlagsSet)
{
if (Parent != null)
{
ActivityTraceFlags = Parent.ActivityTraceFlags;
}
else if (_parentId != null && IsW3CId(_parentId))
{
if (HexConverter.IsHexLowerChar(_parentId[53]) && HexConverter.IsHexLowerChar(_parentId[54]))
{
_w3CIdFlags = (byte)(ActivityTraceId.HexByteFromChars(_parentId[53], _parentId[54]) | ActivityTraceFlagsIsSet);
}
else
{
_w3CIdFlags = ActivityTraceFlagsIsSet;
}
}
}
}
private bool W3CIdFlagsSet
{
get => (_w3CIdFlags & ActivityTraceFlagsIsSet) != 0;
}
/// <summary>
/// Indicates whether this <see cref="Activity"/> object is stopped
/// </summary>
/// <remarks>
/// When subscribing to <see cref="Activity"/> stop event using <see cref="ActivityListener.ActivityStopped"/>, the received <see cref="Activity"/> object in the event callback will have <see cref="IsStopped"/> as true.
/// </remarks>
public bool IsStopped
{
get => (_state & State.IsStopped) != 0;
private set
{
if (value)
{
_state |= State.IsStopped;
}
else
{
_state &= ~State.IsStopped;
}
}
}
/// <summary>
/// Returns the format for the ID.
/// </summary>
public ActivityIdFormat IdFormat
{
get => (ActivityIdFormat)(_state & State.FormatFlags);
private set => _state = (_state & ~State.FormatFlags) | (State)((byte)value & (byte)State.FormatFlags);
}
/// <summary>
/// Enumerates the data stored on an Activity object.
/// </summary>
/// <typeparam name="T">Type being enumerated.</typeparam>
public struct Enumerator<T>
{
private static readonly DiagNode<T> s_Empty = new DiagNode<T>(default!);
private DiagNode<T>? _nextNode;
private DiagNode<T> _currentNode;
internal Enumerator(DiagNode<T>? head)
{
_nextNode = head;
_currentNode = s_Empty;
}
/// <summary>
/// Returns an enumerator that iterates through the data stored on an Activity object.
/// </summary>
/// <returns><see cref="Enumerator{T}"/>.</returns>
[ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)] // Only here to make foreach work
public readonly Enumerator<T> GetEnumerator() => this;
/// <summary>
/// Gets the element at the current position of the enumerator.
/// </summary>
public readonly ref T Current => ref _currentNode.Value;
/// <summary>
/// Advances the enumerator to the next element of the data.
/// </summary>
/// <returns><see langword="true"/> if the enumerator was successfully advanced to the
/// next element; <see langword="false"/> if the enumerator has passed the end of the
/// collection.</returns>
public bool MoveNext()
{
if (_nextNode == null)
{
_currentNode = s_Empty;
return false;
}
_currentNode = _nextNode;
_nextNode = _nextNode.Next;
return true;
}
}
private sealed class BaggageLinkedList : IEnumerable<KeyValuePair<string, string?>>
{
private DiagNode<KeyValuePair<string, string?>>? _first;
public BaggageLinkedList(KeyValuePair<string, string?> firstValue, bool set = false) => _first = ((set && firstValue.Value == null) ? null : new DiagNode<KeyValuePair<string, string?>>(firstValue));
public DiagNode<KeyValuePair<string, string?>>? First => _first;
public void Add(KeyValuePair<string, string?> value)
{
DiagNode<KeyValuePair<string, string?>> newNode = new DiagNode<KeyValuePair<string, string?>>(value);
lock (this)
{
newNode.Next = _first;
_first = newNode;
}
}
public void Set(KeyValuePair<string, string?> value)
{
if (value.Value == null)
{
Remove(value.Key);
return;
}
lock (this)
{
DiagNode<KeyValuePair<string, string?>>? current = _first;
while (current != null)
{
if (current.Value.Key == value.Key)
{
current.Value = value;
return;
}
current = current.Next;
}
DiagNode<KeyValuePair<string, string?>> newNode = new DiagNode<KeyValuePair<string, string?>>(value);
newNode.Next = _first;
_first = newNode;
}
}
public void Remove(string key)
{
lock (this)
{
if (_first == null)
{
return;
}
if (_first.Value.Key == key)
{
_first = _first.Next;
return;
}
DiagNode<KeyValuePair<string, string?>> previous = _first;
while (previous.Next != null)
{
if (previous.Next.Value.Key == key)
{
previous.Next = previous.Next.Next;
return;
}
previous = previous.Next;
}
}
}
public DiagEnumerator<KeyValuePair<string, string?>> GetEnumerator() => new DiagEnumerator<KeyValuePair<string, string?>>(_first);
IEnumerator<KeyValuePair<string, string?>> IEnumerable<KeyValuePair<string, string?>>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
internal sealed class TagsLinkedList : IEnumerable<KeyValuePair<string, object?>>
{
private DiagNode<KeyValuePair<string, object?>>? _first;
private DiagNode<KeyValuePair<string, object?>>? _last;
private StringBuilder? _stringBuilder;
public TagsLinkedList(KeyValuePair<string, object?> firstValue, bool set = false) => _last = _first = ((set && firstValue.Value == null) ? null : new DiagNode<KeyValuePair<string, object?>>(firstValue));
public TagsLinkedList(IEnumerator<KeyValuePair<string, object?>> e)
{
_last = _first = new DiagNode<KeyValuePair<string, object?>>(e.Current);
while (e.MoveNext())
{
_last.Next = new DiagNode<KeyValuePair<string, object?>>(e.Current);
_last = _last.Next;
}
}
public DiagNode<KeyValuePair<string, object?>>? First => _first;
public TagsLinkedList(IEnumerable<KeyValuePair<string, object?>> list) => Add(list);
// Add doesn't take the lock because it is called from the Activity creation before sharing the activity object to the caller.
public void Add(IEnumerable<KeyValuePair<string, object?>> list)
{
IEnumerator<KeyValuePair<string, object?>> e = list.GetEnumerator();
if (!e.MoveNext())
{
return;
}
if (_first == null)
{
_last = _first = new DiagNode<KeyValuePair<string, object?>>(e.Current);
}
else
{
_last!.Next = new DiagNode<KeyValuePair<string, object?>>(e.Current);
_last = _last.Next;
}
while (e.MoveNext())
{
_last.Next = new DiagNode<KeyValuePair<string, object?>>(e.Current);
_last = _last.Next;
}
}
public void Add(KeyValuePair<string, object?> value)
{
DiagNode<KeyValuePair<string, object?>> newNode = new DiagNode<KeyValuePair<string, object?>>(value);
lock (this)
{
if (_first == null)
{
_first = _last = newNode;
return;
}
Debug.Assert(_last != null);
_last!.Next = newNode;
_last = newNode;
}
}
public object? Get(string key)
{
// We don't take the lock here so it is possible the Add/Remove operations mutate the list during the Get operation.
DiagNode<KeyValuePair<string, object?>>? current = _first;
while (current != null)
{
if (current.Value.Key == key)
{
return current.Value.Value;
}
current = current.Next;
}
return null;
}
public void Remove(string key)
{
lock (this)
{
if (_first == null)
{
return;
}
if (_first.Value.Key == key)
{
_first = _first.Next;
if (_first is null)
{
_last = null;
}
return;
}
DiagNode<KeyValuePair<string, object?>> previous = _first;
while (previous.Next != null)
{
if (previous.Next.Value.Key == key)
{
if (object.ReferenceEquals(_last, previous.Next))
{
_last = previous;
}
previous.Next = previous.Next.Next;
return;
}
previous = previous.Next;
}
}
}
public void Set(KeyValuePair<string, object?> value)
{
if (value.Value == null)
{
Remove(value.Key);
return;
}
lock (this)
{
DiagNode<KeyValuePair<string, object?>>? current = _first;
while (current != null)
{
if (current.Value.Key == value.Key)
{
current.Value = value;
return;
}
current = current.Next;
}
DiagNode<KeyValuePair<string, object?>> newNode = new DiagNode<KeyValuePair<string, object?>>(value);
if (_first == null)
{
_first = _last = newNode;
return;
}
Debug.Assert(_last != null);
_last!.Next = newNode;
_last = newNode;
}
}
public DiagEnumerator<KeyValuePair<string, object?>> GetEnumerator() => new DiagEnumerator<KeyValuePair<string, object?>>(_first);
IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerable<KeyValuePair<string, string?>> EnumerateStringValues()
{
DiagNode<KeyValuePair<string, object?>>? current = _first;
while (current != null)
{
if (current.Value.Value is string || current.Value.Value == null)
{
yield return new KeyValuePair<string, string?>(current.Value.Key, (string?)current.Value.Value);
}
current = current.Next;
};
}
public override string ToString()
{
lock (this)
{
if (_first == null)
{
return string.Empty;
}
_stringBuilder ??= new StringBuilder();
_stringBuilder.Append(_first.Value.Key);
_stringBuilder.Append(':');
_stringBuilder.Append(_first.Value.Value);
DiagNode<KeyValuePair<string, object?>>? current = _first.Next;
while (current != null)
{
_stringBuilder.Append(", ");
_stringBuilder.Append(current.Value.Key);
_stringBuilder.Append(':');
_stringBuilder.Append(current.Value.Value);
current = current.Next;
}
string result = _stringBuilder.ToString();
_stringBuilder.Clear();
return result;
}
}
}
[Flags]
private enum State : byte
{
None = 0,
FormatUnknown = 0b_0_00000_00,
FormatHierarchical = 0b_0_00000_01,
FormatW3C = 0b_0_00000_10,
FormatFlags = 0b_0_00000_11,
IsStopped = 0b_1_00000_00,
}
}
/// <summary>
/// These flags are defined by the W3C standard along with the ID for the activity.
/// </summary>
[Flags]
public enum ActivityTraceFlags
{
None = 0b_0_0000000,
Recorded = 0b_0_0000001, // The Activity (or more likely its parents) has been marked as useful to record
}
/// <summary>
/// The possibilities for the format of the ID
/// </summary>
public enum ActivityIdFormat
{
Unknown = 0, // ID format is not known.
Hierarchical = 1, //|XXXX.XX.X_X ... see https://github.com/dotnet/runtime/blob/main/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md#id-format
W3C = 2, // 00-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXX-XX see https://w3c.github.io/trace-context/
};
/// <summary>
/// A TraceId is the format the W3C standard requires for its ID for the entire trace.
/// It represents 16 binary bytes of information, typically displayed as 32 characters
/// of Hexadecimal. A TraceId is a STRUCT, and does contain the 16 bytes of binary information
/// so there is value in passing it by reference. It does know how to convert to and
/// from its Hexadecimal string representation, tries to avoid changing formats until
/// it has to, and caches the string representation after it was created.
/// It is mostly useful as an exchange type.
/// </summary>
public readonly struct ActivityTraceId : IEquatable<ActivityTraceId>
{
private readonly string? _hexString;
internal ActivityTraceId(string? hexString) => _hexString = hexString;
/// <summary>
/// Create a new TraceId with at random number in it (very likely to be unique)
/// </summary>
public static ActivityTraceId CreateRandom()
{
Span<byte> span = stackalloc byte[sizeof(ulong) * 2];
SetToRandomBytes(span);
return CreateFromBytes(span);
}
public static ActivityTraceId CreateFromBytes(ReadOnlySpan<byte> idData)
{
if (idData.Length != 16)
throw new ArgumentOutOfRangeException(nameof(idData));
#if NET9_0_OR_GREATER
return new ActivityTraceId(Convert.ToHexStringLower(idData));
#else
return new ActivityTraceId(HexConverter.ToString(idData, HexConverter.Casing.Lower));
#endif
}
public static ActivityTraceId CreateFromUtf8String(ReadOnlySpan<byte> idData) => new ActivityTraceId(idData);
public static ActivityTraceId CreateFromString(ReadOnlySpan<char> idData)
{
if (idData.Length != 32 || !ActivityTraceId.IsLowerCaseHexAndNotAllZeros(idData))
throw new ArgumentOutOfRangeException(nameof(idData));
return new ActivityTraceId(idData.ToString());
}
/// <summary>
/// Returns the TraceId as a 32 character hexadecimal string.
/// </summary>
public string ToHexString()
{
return _hexString ?? "00000000000000000000000000000000";
}
/// <summary>
/// Returns the TraceId as a 32 character hexadecimal string.
/// </summary>
public override string ToString() => ToHexString();
public static bool operator ==(ActivityTraceId traceId1, ActivityTraceId traceId2)
{
return traceId1._hexString == traceId2._hexString;
}
public static bool operator !=(ActivityTraceId traceId1, ActivityTraceId traceId2)
{
return traceId1._hexString != traceId2._hexString;
}
public bool Equals(ActivityTraceId traceId)
{
return _hexString == traceId._hexString;
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
if (obj is ActivityTraceId traceId)
return _hexString == traceId._hexString;
return false;
}
public override int GetHashCode()
{
return ToHexString().GetHashCode();
}
/// <summary>
/// This is exposed as CreateFromUtf8String, but we are modifying fields, so the code needs to be in a constructor.
/// </summary>
/// <param name="idData"></param>
private ActivityTraceId(ReadOnlySpan<byte> idData)
{
if (idData.Length != 32)
throw new ArgumentOutOfRangeException(nameof(idData));
Span<ulong> span = stackalloc ulong[2];
if (!Utf8Parser.TryParse(idData.Slice(0, 16), out span[0], out _, 'x'))
{
// Invalid Id, use random https://github.com/dotnet/runtime/issues/29859
_hexString = CreateRandom()._hexString;
return;
}
if (!Utf8Parser.TryParse(idData.Slice(16, 16), out span[1], out _, 'x'))
{
// Invalid Id, use random https://github.com/dotnet/runtime/issues/29859
_hexString = CreateRandom()._hexString;
return;
}
if (BitConverter.IsLittleEndian)
{
span[0] = BinaryPrimitives.ReverseEndianness(span[0]);
span[1] = BinaryPrimitives.ReverseEndianness(span[1]);
}
#if NET9_0_OR_GREATER
_hexString = Convert.ToHexStringLower(MemoryMarshal.AsBytes(span));
#else
_hexString = HexConverter.ToString(MemoryMarshal.AsBytes(span), HexConverter.Casing.Lower);
#endif
}
/// <summary>
/// Copy the bytes of the TraceId (16 total) into the 'destination' span.
/// </summary>
public void CopyTo(Span<byte> destination)
{
ActivityTraceId.SetSpanFromHexChars(ToHexString().AsSpan(), destination);
}
/// <summary>
/// Sets the bytes in 'outBytes' to be random values. outBytes.Length must be either 8 or 16 bytes.
/// </summary>
/// <param name="outBytes"></param>
internal static unsafe void SetToRandomBytes(Span<byte> outBytes)
{
Debug.Assert(outBytes.Length == 16 || outBytes.Length == 8);
RandomNumberGenerator r = RandomNumberGenerator.Current;
Unsafe.WriteUnaligned(ref outBytes[0], r.Next());
if (outBytes.Length == 16)
{
Unsafe.WriteUnaligned(ref outBytes[8], r.Next());
}
}
/// <summary>
/// Converts 'idData' which is assumed to be HEX Unicode characters to binary
/// puts it in 'outBytes'
/// </summary>
internal static void SetSpanFromHexChars(ReadOnlySpan<char> charData, Span<byte> outBytes)
{
Debug.Assert(outBytes.Length * 2 == charData.Length);
for (int i = 0; i < outBytes.Length; i++)
outBytes[i] = HexByteFromChars(charData[i * 2], charData[i * 2 + 1]);
}
internal static byte HexByteFromChars(char char1, char char2)
{
int hi = HexConverter.FromLowerChar(char1);
int lo = HexConverter.FromLowerChar(char2);
if ((hi | lo) == 0xFF)
{
throw new ArgumentOutOfRangeException("idData");
}
return (byte)((hi << 4) | lo);
}
internal static bool IsLowerCaseHexAndNotAllZeros(ReadOnlySpan<char> idData)
{
// Verify lower-case hex and not all zeros https://w3c.github.io/trace-context/#field-value
bool isNonZero = false;
int i = 0;
for (; i < idData.Length; i++)
{
char c = idData[i];
if (!HexConverter.IsHexLowerChar(c))
{
return false;
}
if (c != '0')
{
isNonZero = true;
}
}
return isNonZero;
}
}
/// <summary>
/// A SpanId is the format the W3C standard requires for its ID for a single span in a trace.
/// It represents 8 binary bytes of information, typically displayed as 16 characters
/// of Hexadecimal. A SpanId is a STRUCT, and does contain the 8 bytes of binary information
/// so there is value in passing it by reference. It does know how to convert to and
/// from its Hexadecimal string representation, tries to avoid changing formats until
/// it has to, and caches the string representation after it was created.
/// It is mostly useful as an exchange type.
/// </summary>
public readonly struct ActivitySpanId : IEquatable<ActivitySpanId>
{
private readonly string? _hexString;
internal ActivitySpanId(string? hexString) => _hexString = hexString;
/// <summary>
/// Create a new SpanId with at random number in it (very likely to be unique)
/// </summary>
public static unsafe ActivitySpanId CreateRandom()
{
ulong id;
ActivityTraceId.SetToRandomBytes(new Span<byte>(&id, sizeof(ulong)));
#if NET9_0_OR_GREATER
return new ActivitySpanId(Convert.ToHexStringLower(new ReadOnlySpan<byte>(&id, sizeof(ulong))));
#else
return new ActivitySpanId(HexConverter.ToString(new ReadOnlySpan<byte>(&id, sizeof(ulong)), HexConverter.Casing.Lower));
#endif
}
public static ActivitySpanId CreateFromBytes(ReadOnlySpan<byte> idData)
{
if (idData.Length != 8)
throw new ArgumentOutOfRangeException(nameof(idData));
#if NET9_0_OR_GREATER
return new ActivitySpanId(Convert.ToHexStringLower(idData));
#else
return new ActivitySpanId(HexConverter.ToString(idData, HexConverter.Casing.Lower));
#endif
}
public static ActivitySpanId CreateFromUtf8String(ReadOnlySpan<byte> idData) => new ActivitySpanId(idData);
public static ActivitySpanId CreateFromString(ReadOnlySpan<char> idData)
{
if (idData.Length != 16 || !ActivityTraceId.IsLowerCaseHexAndNotAllZeros(idData))
throw new ArgumentOutOfRangeException(nameof(idData));
return new ActivitySpanId(idData.ToString());
}
/// <summary>
/// Returns the SpanId as a 16 character hexadecimal string.
/// </summary>
/// <returns></returns>
public string ToHexString()
{
return _hexString ?? "0000000000000000";
}
/// <summary>
/// Returns SpanId as a hex string.
/// </summary>
public override string ToString() => ToHexString();
public static bool operator ==(ActivitySpanId spanId1, ActivitySpanId spandId2)
{
return spanId1._hexString == spandId2._hexString;
}
public static bool operator !=(ActivitySpanId spanId1, ActivitySpanId spandId2)
{
return spanId1._hexString != spandId2._hexString;
}
public bool Equals(ActivitySpanId spanId)
{
return _hexString == spanId._hexString;
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
if (obj is ActivitySpanId spanId)
return _hexString == spanId._hexString;
return false;
}
public override int GetHashCode()
{
return ToHexString().GetHashCode();
}
private unsafe ActivitySpanId(ReadOnlySpan<byte> idData)
{
if (idData.Length != 16)
{
throw new ArgumentOutOfRangeException(nameof(idData));
}
if (!Utf8Parser.TryParse(idData, out ulong id, out _, 'x'))
{
// Invalid Id, use random https://github.com/dotnet/runtime/issues/29859
_hexString = CreateRandom()._hexString;
return;
}
if (BitConverter.IsLittleEndian)
{
id = BinaryPrimitives.ReverseEndianness(id);
}
#if NET9_0_OR_GREATER
_hexString = Convert.ToHexStringLower(new ReadOnlySpan<byte>(&id, sizeof(ulong)));
#else
_hexString = HexConverter.ToString(new ReadOnlySpan<byte>(&id, sizeof(ulong)), HexConverter.Casing.Lower);
#endif
}
/// <summary>
/// Copy the bytes of the TraceId (8 bytes total) into the 'destination' span.
/// </summary>
public void CopyTo(Span<byte> destination)
{
ActivityTraceId.SetSpanFromHexChars(ToHexString().AsSpan(), destination);
}
}
}
|