| | | 1 | | |
| | | 2 | | using System; |
| | | 3 | | |
| | | 4 | | namespace AmbientServices; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// An interface that abstracts a service profiler notification sink. |
| | | 8 | | /// </summary> |
| | | 9 | | /// <remarks> |
| | | 10 | | /// <pitch>The push side of profiling: implement this to receive every system switch as it happens and accumulate it how |
| | | 11 | | /// <pledge> |
| | | 12 | | /// <see cref="OnSystemSwitched"/> is called once per switch, after the switch has been applied. Each call delivers a c |
| | | 13 | | /// Calls may arrive concurrently from multiple call contexts and are not guaranteed to be ordered by timestamp across c |
| | | 14 | | /// </pledge> |
| | | 15 | | /// </remarks> |
| | | 16 | | public interface IAmbientServiceProfilerNotificationSink |
| | | 17 | | { |
| | | 18 | | /// <summary> |
| | | 19 | | /// Notifies the notification sink that the system has switched. |
| | | 20 | | /// </summary> |
| | | 21 | | /// <remarks> |
| | | 22 | | /// This function will be called whenever the service profiler is told that the currently-processing system has swit |
| | | 23 | | /// Note that the previously-executing system may or may not be revised at this time. |
| | | 24 | | /// Such revisions can be used to distinguish between processing that resulted in success or failure, or other simil |
| | | 25 | | /// </remarks> |
| | | 26 | | /// <param name="newSystemStartStopwatchTimestamp">The stopwatch timestamp when the new system started.</param> |
| | | 27 | | /// <param name="newSystem">The identifier for the system that is starting to run.</param> |
| | | 28 | | /// <param name="oldSystemStartStopwatchTimestamp">The stopwatch timestamp when the old system started running.</par |
| | | 29 | | /// <param name="oldSystem">The identifier for the system that has just finished running, as known to the call conte |
| | | 30 | | /// <param name="revisedOldSystem">An optional revised name for the system that has just finished running that overr |
| | | 31 | | void OnSystemSwitched(long newSystemStartStopwatchTimestamp, string newSystem, long oldSystemStartStopwatchTimestamp |
| | | 32 | | } |
| | | 33 | | /// <summary> |
| | | 34 | | /// An interface that abstracts a service profiler service. |
| | | 35 | | /// </summary> |
| | | 36 | | /// <remarks> |
| | | 37 | | /// <pitch> |
| | | 38 | | /// Cheap, always-on attribution of where a request's time goes across backend systems (databases, caches, remote servic |
| | | 39 | | /// It deliberately does <em>not</em> measure CPU time, allocations, or call counts at finer than system-switch granular |
| | | 40 | | /// </pitch> |
| | | 41 | | /// <pledge> |
| | | 42 | | /// Within a single call context exactly one system is active at a time: <see cref="SwitchSystem"/> ends the currently-a |
| | | 43 | | /// Concurrency is expressed only by multiple call contexts, each internally sequential; an <see cref="System.Threading. |
| | | 44 | | /// Registered <see cref="IAmbientServiceProfilerNotificationSink"/> instances are notified on every switch with the ide |
| | | 45 | | /// </pledge> |
| | | 46 | | /// <priority> |
| | | 47 | | /// 1. Always-on affordability over measurement richness: the point of this service is that it can be left enabled in pr |
| | | 48 | | /// 2. Self-contained notifications over a narrow notification surface: every switch tells sinks the identities and star |
| | | 49 | | /// </priority> |
| | | 50 | | /// </remarks> |
| | | 51 | | public interface IAmbientServiceProfiler |
| | | 52 | | { |
| | | 53 | | /// <summary> |
| | | 54 | | /// Gets the identifier of the system currently active in this call context, or null/empty when the default |
| | | 55 | | /// (unattributed) system is active. Used by <see cref="ScopedSystemSwitch"/> to capture and restore the prior |
| | | 56 | | /// system on dispose so that switch scopes nest correctly. |
| | | 57 | | /// </summary> |
| | | 58 | | string? CurrentSystem { get; } |
| | | 59 | | /// <summary> |
| | | 60 | | /// Switches the system that is executing in this call context. |
| | | 61 | | /// </summary> |
| | | 62 | | /// <param name="system">A string indicating which system is beginning to execute, or null or empty string to indica |
| | | 63 | | /// <param name="updatedPreviousSystem">An optional updated for the previous system in case part of the system ident |
| | | 64 | | /// <remarks> |
| | | 65 | | /// The system should be identified using the following form: |
| | | 66 | | /// MainSystem/[Subsystem Type:]Subsystem/[Subsystem Type:]Subsystem/[Subsystem Type:]Subsystem/[Subsystem Type:]Sub |
| | | 67 | | /// For example: |
| | | 68 | | /// DynamoDB/Table:My-table/Partition:342644/Result:Success |
| | | 69 | | /// S3/Bucket:My-bucket/Prefix:abcdefg/Result:Retry |
| | | 70 | | /// SQL/Database:My-database/Table:User/Result:Failed |
| | | 71 | | /// In the analysis pipeline, systems are grouped using the system group transform Regex from the settings. |
| | | 72 | | /// Any matching Regex match groups found by the regex expression will be concatenated into the transformed string. |
| | | 73 | | /// To pass the system string through unaltered (as its own group), use null, empty string, or .* as the system grou |
| | | 74 | | /// For example, to transform the group systems by only the main system, database, bucket, and result, while retaini |
| | | 75 | | /// (?:([^:/]+)(?:(/Database:[^:/]*)|(/Bucket:[^:/]*)|(/Result:[^:/]*)|(?:/[^/]*))*) |
| | | 76 | | /// For example, to transform the group systems by only the main system, database, bucket, and result, without retai |
| | | 77 | | /// (?:([^:/]+)(?:(?:(/)(?:Database:)([^:/]*))|(?:(/)(?:Bucket:)([^:/]*))|(?:(/)(?:Result:)([^:/]*))|(?:/[^/]*))*) |
| | | 78 | | /// </remarks> |
| | | 79 | | void SwitchSystem(string? system, string? updatedPreviousSystem = null); |
| | | 80 | | /// <summary> |
| | | 81 | | /// Re-baselines the current call context as a freshly-forked context: the active system becomes the default (null) |
| | | 82 | | /// system starting now, and the interval the context inherited from its parent (the parent's active system, timed |
| | | 83 | | /// since the parent last switched) is deliberately <em>not</em> recorded. |
| | | 84 | | /// </summary> |
| | | 85 | | /// <remarks> |
| | | 86 | | /// Call this at the start of forked work (after execution has actually crossed onto the forked context, e.g. after |
| | | 87 | | /// the first <c>await</c>). A forked <see cref="System.Threading.ExecutionContext"/> inherits the parent's |
| | | 88 | | /// active-system start timestamp via <see cref="System.Threading.AsyncLocal{T}"/> flow; when many forks share that |
| | | 89 | | /// inherited start, each one's first <see cref="SwitchSystem"/> would close an interval reaching back to the parent |
| | | 90 | | /// start, so the parent's pre-fork time gets charged once per fork and the default group's aggregate (busy) time |
| | | 91 | | /// balloons. Unlike <see cref="SwitchSystem"/>, this records no interval — it only discards the inherited start, s |
| | | 92 | | /// the parent (which keeps its own copy) still accounts for that span exactly once. No notification sink is called |
| | | 93 | | /// </remarks> |
| | | 94 | | void ResetForkedCallContext(); |
| | | 95 | | /// <summary> |
| | | 96 | | /// Registers a system switch notification sink with this ambient service profiler. |
| | | 97 | | /// </summary> |
| | | 98 | | /// <param name="sink">An <see cref="IAmbientServiceProfilerNotificationSink"/> that will receive notifications when |
| | | 99 | | /// <returns>true if the registration was successful, false if the specified sink was already registered.</returns> |
| | | 100 | | bool RegisterSystemSwitchedNotificationSink(IAmbientServiceProfilerNotificationSink sink); |
| | | 101 | | /// <summary> |
| | | 102 | | /// Deregisters an access notification sink with this ambient service profiler. |
| | | 103 | | /// </summary> |
| | | 104 | | /// <param name="sink">An <see cref="IAmbientServiceProfilerNotificationSink"/> that will receive notifications when |
| | | 105 | | /// <returns>true if the deregistration was successful, false if the specified sink was not registered.</returns> |
| | | 106 | | bool DeregisterSystemSwitchedNotificationSink(IAmbientServiceProfilerNotificationSink sink); |
| | | 107 | | #if NET5_0_OR_GREATER |
| | | 108 | | /// <summary> |
| | | 109 | | /// Scopes a switch to a specified system to simplify the syntax of tracking system usage when the full system ident |
| | | 110 | | /// </summary> |
| | | 111 | | /// <param name="system">A string indicating which system is beginning to execute, or null or empty string to indica |
| | | 112 | | /// <param name="updatedPreviousSystem">An optional updated for the previous system in case part of the system ident |
| | | 113 | | /// <returns>A <see cref="ScopedSystemSwitch"/> that should be disposed when usage of the system is finished.</retur |
| | | 114 | | ScopedSystemSwitch ScopedSystemSwitch(string? system, string? updatedPreviousSystem = null) |
| | | 115 | | { |
| | | 116 | | return new ScopedSystemSwitch(this, system, updatedPreviousSystem); |
| | | 117 | | } |
| | | 118 | | #endif |
| | | 119 | | } |
| | | 120 | | /// <summary> |
| | | 121 | | /// A disposable scoping class that lets the caller indicate when system usage is complete. |
| | | 122 | | /// </summary> |
| | | 123 | | /// <remarks> |
| | | 124 | | /// <pitch>The ergonomic way to mark a system active for the duration of a <c>using</c> block when the full system ident |
| | | 125 | | /// <pledge>Construction captures the currently-active system (via <see cref="IAmbientServiceProfiler.CurrentSystem"/>) |
| | | 126 | | /// <plan>A thin wrapper that holds the <see cref="IAmbientServiceProfiler"/>, the system it switched to, and the system |
| | | 127 | | /// </remarks> |
| | | 128 | | public sealed class ScopedSystemSwitch : IDisposable |
| | | 129 | | { |
| | | 130 | | private readonly IAmbientServiceProfiler _profiler; |
| | | 131 | | private readonly string? _system; |
| | | 132 | | private readonly string? _previousSystem; |
| | | 133 | | |
| | | 134 | | /// <summary> |
| | | 135 | | /// Constructs a scoped system switcher. |
| | | 136 | | /// </summary> |
| | | 137 | | /// <param name="profiler">The <see cref="IAmbientServiceProfiler"/> to use.</param> |
| | | 138 | | /// <param name="system">The name of the system that will execute during the scope.</param> |
| | | 139 | | /// <param name="updatedPreviousSystem">An update to the previously-running system in case the full name was not kno |
| | | 140 | | /// <exception cref="ArgumentNullException"><paramref name="profiler"/> is null.</exception> |
| | 2 | 141 | | public ScopedSystemSwitch(IAmbientServiceProfiler profiler, string? system, string? updatedPreviousSystem = null) |
| | | 142 | | { |
| | 2 | 143 | | if (profiler == null) throw new ArgumentNullException(nameof(profiler)); |
| | 2 | 144 | | _profiler = profiler; |
| | 2 | 145 | | _system = system; |
| | | 146 | | // capture the system active before this scope so disposal can restore it, allowing scopes to nest |
| | 2 | 147 | | _previousSystem = profiler.CurrentSystem; |
| | 2 | 148 | | profiler.SwitchSystem(system, updatedPreviousSystem); |
| | 2 | 149 | | } |
| | | 150 | | /// <summary> |
| | | 151 | | /// Disposes of the instance, restoring the system that was active when the scope was constructed. |
| | | 152 | | /// </summary> |
| | | 153 | | public void Dispose() |
| | | 154 | | { |
| | | 155 | | // Restore the previously-active system (so scopes nest) and replay the system we opened as the revised |
| | | 156 | | // previous system so the collector attributes this scope's interval from the self-contained switch event |
| | | 157 | | // instead of the per-context active-system map, which last-writer-wins across fan-out children that share |
| | | 158 | | // an inherited call-context key. |
| | 2 | 159 | | _profiler.SwitchSystem(_previousSystem, _system); |
| | 2 | 160 | | } |
| | | 161 | | } |