1+ using Microsoft . Extensions . Hosting ;
2+ using Microsoft . Extensions . Logging ;
3+ using System . Collections . Concurrent ;
4+ using System . Diagnostics . Metrics ;
5+ using System . Runtime . ExceptionServices ;
6+ using System . Runtime . InteropServices ;
7+
8+ namespace BookLibrary . Infrastructure . OpenTelemetry ;
9+
10+ internal sealed partial class ExceptionTracker : IDisposable
11+ {
12+ private readonly Meter _meter ;
13+ private readonly ILogger < ExceptionTracker > _logger ;
14+
15+ private readonly Counter < long > _exceptionsCounter ;
16+
17+ private static readonly ConcurrentDictionary < Type , string > _trimmedTypeNames = new ( ) ;
18+
19+ private const int LABEL_MAX_LENGTH = 127 ;
20+ private const int LOG_EVERY_N_EXCEPTIONS = 100 ;
21+
22+ [ ThreadStatic ]
23+ private static bool _handlingFirstChanceException ;
24+
25+ [ ThreadStatic ]
26+ private static Dictionary < Type , long > ? _sampler ;
27+
28+ private static bool _started ;
29+
30+ public ExceptionTracker (
31+ IMeterFactory meterFactory ,
32+ ILogger < ExceptionTracker > logger
33+ )
34+ {
35+ _logger = logger ;
36+
37+ _meter = meterFactory . Create ( GetType ( ) . FullName ! ) ;
38+ _exceptionsCounter = _meter . CreateCounter < long > ( name : "dotnet.exceptions" , description : "Count of throwed exceptions." ) ;
39+ }
40+
41+ public void Dispose ( )
42+ {
43+ _meter . Dispose ( ) ;
44+
45+ Stop ( ) ;
46+ }
47+
48+ public void Start ( )
49+ {
50+ if ( _started )
51+ {
52+ return ;
53+ }
54+
55+ _started = true ;
56+ AppDomain . CurrentDomain . FirstChanceException += HandleFirstChanceException ;
57+ }
58+
59+ public void Stop ( )
60+ {
61+ if ( ! _started )
62+ {
63+ return ;
64+ }
65+
66+ AppDomain . CurrentDomain . FirstChanceException -= HandleFirstChanceException ;
67+ }
68+
69+ private void HandleFirstChanceException ( object ? sender , FirstChanceExceptionEventArgs e )
70+ {
71+ if ( _handlingFirstChanceException )
72+ {
73+ return ;
74+ }
75+
76+ if ( e . Exception is TaskCanceledException or OperationCanceledException )
77+ {
78+ return ;
79+ }
80+
81+ _handlingFirstChanceException = true ;
82+
83+ var exceptionType = e . Exception . GetType ( ) ;
84+
85+ _sampler ??= new Dictionary < Type , long > ( ) ;
86+
87+ ref var counter = ref CollectionsMarshal . GetValueRefOrAddDefault ( _sampler , exceptionType , out var exists ) ;
88+
89+ var needLog = ! exists || Interlocked . Increment ( ref counter ) % LOG_EVERY_N_EXCEPTIONS == 0 ;
90+
91+ if ( needLog )
92+ {
93+ LogExceptionSample ( e . Exception , exceptionType . FullName ) ;
94+ }
95+
96+ _exceptionsCounter . Add ( 1 , new KeyValuePair < string , object ? > ( "error_type" , Trim ( exceptionType ) ) ) ;
97+
98+ _handlingFirstChanceException = false ;
99+ }
100+
101+ private static string Trim ( Type type )
102+ {
103+ return _trimmedTypeNames . GetOrAdd ( type , static type =>
104+ {
105+ var typeFullName = type . FullName ! ;
106+
107+ Span < char > typeLabel = stackalloc char [ Math . Min ( LABEL_MAX_LENGTH , typeFullName . Length ) ] ;
108+
109+ typeFullName . AsSpan ( 0 , typeLabel . Length ) . CopyTo ( typeLabel ) ;
110+
111+ var writePos = 0 ;
112+ for ( var readPos = 0 ; readPos < typeLabel . Length ; readPos ++ )
113+ {
114+ var c = typeLabel [ readPos ] ;
115+
116+ if ( char . IsAscii ( c ) )
117+ {
118+ typeLabel [ writePos ++ ] = c ;
119+ }
120+ }
121+
122+ typeLabel = typeLabel [ ..writePos ] ;
123+
124+ return typeLabel . ToString ( ) ;
125+ } ) ;
126+ }
127+
128+ [ LoggerMessage (
129+ eventId : 0 ,
130+ level : LogLevel . Information ,
131+ message : "Sample of throwed exception {ExceptionType}"
132+ ) ]
133+ private partial void LogExceptionSample ( Exception ex , string ? exceptionType ) ;
134+ }
135+
136+ internal sealed class ExceptionTrackerLifecycle : IHostedService
137+ {
138+ private readonly ExceptionTracker _exceptionTracker ;
139+
140+ public ExceptionTrackerLifecycle ( ExceptionTracker exceptionTracker )
141+ {
142+ _exceptionTracker = exceptionTracker ;
143+ }
144+
145+ public Task StartAsync ( CancellationToken cancellationToken )
146+ {
147+ _exceptionTracker . Start ( ) ;
148+
149+ return Task . CompletedTask ;
150+ }
151+
152+ public Task StopAsync ( CancellationToken cancellationToken )
153+ {
154+ _exceptionTracker . Stop ( ) ;
155+
156+ return Task . CompletedTask ;
157+ }
158+ }
0 commit comments