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+ try
84+ {
85+ var exceptionType = e . Exception . GetType ( ) ;
86+
87+ _sampler ??= new Dictionary < Type , long > ( ) ;
88+
89+ ref var counter = ref CollectionsMarshal . GetValueRefOrAddDefault ( _sampler , exceptionType , out var exists ) ;
90+
91+ var needLog = ! exists || Interlocked . Increment ( ref counter ) % LOG_EVERY_N_EXCEPTIONS == 0 ;
92+
93+ if ( needLog )
94+ {
95+ LogExceptionSample ( e . Exception , exceptionType . FullName ) ;
96+ }
97+
98+ _exceptionsCounter . Add ( 1 , new KeyValuePair < string , object ? > ( "error_type" , Trim ( exceptionType ) ) ) ;
99+ }
100+ catch ( Exception )
101+ {
102+ // skip
103+ }
104+ finally
105+ {
106+ _handlingFirstChanceException = false ;
107+ }
108+ }
109+
110+ private static string Trim ( Type type )
111+ {
112+ return _trimmedTypeNames . GetOrAdd ( type , static type =>
113+ {
114+ var typeFullName = type . FullName ! ;
115+
116+ Span < char > typeLabel = stackalloc char [ Math . Min ( LABEL_MAX_LENGTH , typeFullName . Length ) ] ;
117+
118+ typeFullName . AsSpan ( 0 , typeLabel . Length ) . CopyTo ( typeLabel ) ;
119+
120+ var writePos = 0 ;
121+ for ( var readPos = 0 ; readPos < typeLabel . Length ; readPos ++ )
122+ {
123+ var c = typeLabel [ readPos ] ;
124+
125+ if ( char . IsAscii ( c ) )
126+ {
127+ typeLabel [ writePos ++ ] = c ;
128+ }
129+ }
130+
131+ typeLabel = typeLabel [ ..writePos ] ;
132+
133+ return typeLabel . ToString ( ) ;
134+ } ) ;
135+ }
136+
137+ [ LoggerMessage (
138+ eventId : 0 ,
139+ level : LogLevel . Information ,
140+ message : "Sample of throwed exception {ExceptionType}"
141+ ) ]
142+ private partial void LogExceptionSample ( Exception ex , string ? exceptionType ) ;
143+ }
144+
145+ internal sealed class ExceptionTrackerLifecycle : IHostedService
146+ {
147+ private readonly ExceptionTracker _exceptionTracker ;
148+
149+ public ExceptionTrackerLifecycle ( ExceptionTracker exceptionTracker )
150+ {
151+ _exceptionTracker = exceptionTracker ;
152+ }
153+
154+ public Task StartAsync ( CancellationToken cancellationToken )
155+ {
156+ _exceptionTracker . Start ( ) ;
157+
158+ return Task . CompletedTask ;
159+ }
160+
161+ public Task StopAsync ( CancellationToken cancellationToken )
162+ {
163+ _exceptionTracker . Stop ( ) ;
164+
165+ return Task . CompletedTask ;
166+ }
167+ }
0 commit comments