1+ using System . Net . Http . Json ;
2+ using System . Reflection ;
3+ using DebugProbe . AspNetCore . Models ;
4+ using DebugProbe . AspNetCore . Options ;
5+
6+ namespace DebugProbe . AspNetCore . Ingestion ;
7+
8+ public sealed class DebugProbeServerClient
9+ {
10+ private const string BodyTooLargeMessage = "[Body too large]" ;
11+ private const string BinaryBodyMessage = "[Body not captured: non-text content]" ;
12+
13+ private static readonly HttpClient Http = new ( )
14+ {
15+ Timeout = TimeSpan . FromSeconds ( 2 )
16+ } ;
17+
18+ private readonly DebugProbeOptions _options ;
19+
20+ public DebugProbeServerClient ( DebugProbeOptions options )
21+ {
22+ _options = options ;
23+ }
24+
25+ public async Task SendRequestAsync ( DebugEntry entry , DebugEnvironment environment )
26+ {
27+ if ( ! TryGetEndpoint ( out var endpoint ) )
28+ {
29+ return ;
30+ }
31+
32+ try
33+ {
34+ await Http . PostAsJsonAsync ( endpoint , MapRequest ( entry , environment ) ) ;
35+ }
36+ catch
37+ {
38+ // Central ingestion is optional and must never break the local application.
39+ }
40+ }
41+
42+ private bool TryGetEndpoint ( out Uri endpoint )
43+ {
44+ endpoint = default ! ;
45+
46+ if ( string . IsNullOrWhiteSpace ( _options . ServerUrl ) ||
47+ ! Uri . TryCreate ( _options . ServerUrl , UriKind . Absolute , out var serverUri ) )
48+ {
49+ return false ;
50+ }
51+
52+ endpoint = new Uri ( EnsureTrailingSlash ( serverUri ) , "api/ingestion/requests" ) ;
53+ return true ;
54+ }
55+
56+ private RequestData MapRequest ( DebugEntry entry , DebugEnvironment environment )
57+ {
58+ return new RequestData
59+ {
60+ Application = MapApplication ( environment ) ,
61+ TimestampUtc = entry . RequestTimeUtc . ToUniversalTime ( ) ,
62+ RequestId = entry . Id ,
63+ Method = entry . Method ,
64+ Path = entry . Path ,
65+ Query = entry . Query ,
66+ Url = entry . RequestUrl ,
67+ DurationMs = entry . DurationMs ,
68+ StatusCode = entry . StatusCode ,
69+ RequestBody = MapBody ( entry . RequestBody , entry . RequestSize ) ,
70+ ResponseBody = MapBody ( entry . ResponseBody , entry . ResponseSize ) ,
71+ RequestHeaders = entry . RequestHeaders ,
72+ ResponseHeaders = entry . ResponseHeaders ,
73+ OutgoingRequests = entry . OutgoingRequests . Select ( MapOutgoingRequest ) . ToList ( )
74+ } ;
75+ }
76+
77+ private ApplicationData MapApplication ( DebugEnvironment environment )
78+ {
79+ var assemblyName = Assembly . GetEntryAssembly ( ) ? . GetName ( ) . Name ?? "Application" ;
80+
81+ return new ApplicationData
82+ {
83+ ApplicationId = string . IsNullOrWhiteSpace ( _options . ApplicationId ) ? assemblyName : _options . ApplicationId ,
84+ ApplicationName = string . IsNullOrWhiteSpace ( _options . ApplicationName ) ? assemblyName : _options . ApplicationName ,
85+ Environment = environment . Environment ,
86+ InstanceId = _options . InstanceId ,
87+ MachineName = environment . MachineName ,
88+ AssemblyVersion = environment . AssemblyVersion ,
89+ Culture = environment . Culture ,
90+ UiCulture = environment . UiCulture ,
91+ TimeZone = environment . TimeZone
92+ } ;
93+ }
94+
95+ private static OutgoingRequestData MapOutgoingRequest ( DebugOutgoingRequest outgoing )
96+ {
97+ return new OutgoingRequestData
98+ {
99+ Method = outgoing . Method ,
100+ Url = outgoing . Url ,
101+ StatusCode = outgoing . StatusCode ,
102+ DurationMs = outgoing . DurationMs ,
103+ TimestampUtc = new DateTimeOffset ( DateTime . SpecifyKind ( outgoing . TimestampUtc , DateTimeKind . Utc ) ) ,
104+ IsSuccessStatusCode = outgoing . IsSuccessStatusCode ,
105+ RequestBody = MapBody ( outgoing . RequestBody , null ) ,
106+ ResponseBody = MapBody ( outgoing . ResponseBody , null ) ,
107+ Exception = outgoing . Exception ,
108+ RequestHeaders = outgoing . RequestHeaders ,
109+ ResponseHeaders = outgoing . ResponseHeaders
110+ } ;
111+ }
112+
113+ private static BodyData ? MapBody ( string ? content , long ? sizeBytes )
114+ {
115+ if ( content is null )
116+ {
117+ return null ;
118+ }
119+
120+ return new BodyData
121+ {
122+ SizeBytes = sizeBytes ,
123+ Captured = content . Length > 0 && content != BodyTooLargeMessage && content != BinaryBodyMessage ,
124+ Truncated = content == BodyTooLargeMessage || content . EndsWith ( "[truncated]" , StringComparison . Ordinal ) ,
125+ Content = content
126+ } ;
127+ }
128+
129+ private static Uri EnsureTrailingSlash ( Uri uri )
130+ {
131+ var value = uri . ToString ( ) ;
132+ return value . EndsWith ( "/" , StringComparison . Ordinal ) ? uri : new Uri ( value + "/" ) ;
133+ }
134+
135+ private sealed class RequestData
136+ {
137+ public int SchemaVersion { get ; set ; } = 1 ;
138+ public ApplicationData Application { get ; set ; } = new ( ) ;
139+ public DateTimeOffset TimestampUtc { get ; set ; }
140+ public string ? RequestId { get ; set ; }
141+ public string Method { get ; set ; } = string . Empty ;
142+ public string Path { get ; set ; } = string . Empty ;
143+ public string ? Query { get ; set ; }
144+ public string ? Url { get ; set ; }
145+ public long DurationMs { get ; set ; }
146+ public int StatusCode { get ; set ; }
147+ public BodyData ? RequestBody { get ; set ; }
148+ public BodyData ? ResponseBody { get ; set ; }
149+ public Dictionary < string , string > RequestHeaders { get ; set ; } = [ ] ;
150+ public Dictionary < string , string > ResponseHeaders { get ; set ; } = [ ] ;
151+ public List < OutgoingRequestData > OutgoingRequests { get ; set ; } = [ ] ;
152+ }
153+
154+ private sealed class ApplicationData
155+ {
156+ public string ApplicationId { get ; set ; } = string . Empty ;
157+ public string ApplicationName { get ; set ; } = string . Empty ;
158+ public string ? Environment { get ; set ; }
159+ public string ? InstanceId { get ; set ; }
160+ public string ? MachineName { get ; set ; }
161+ public string ? AssemblyVersion { get ; set ; }
162+ public string ? Culture { get ; set ; }
163+ public string ? UiCulture { get ; set ; }
164+ public string ? TimeZone { get ; set ; }
165+ }
166+
167+ private sealed class BodyData
168+ {
169+ public long ? SizeBytes { get ; set ; }
170+ public bool Captured { get ; set ; }
171+ public bool Truncated { get ; set ; }
172+ public string ? Content { get ; set ; }
173+ }
174+
175+ private sealed class OutgoingRequestData
176+ {
177+ public string Method { get ; set ; } = string . Empty ;
178+ public string Url { get ; set ; } = string . Empty ;
179+ public int ? StatusCode { get ; set ; }
180+ public long DurationMs { get ; set ; }
181+ public DateTimeOffset ? TimestampUtc { get ; set ; }
182+ public bool ? IsSuccessStatusCode { get ; set ; }
183+ public BodyData ? RequestBody { get ; set ; }
184+ public BodyData ? ResponseBody { get ; set ; }
185+ public string ? Exception { get ; set ; }
186+ public Dictionary < string , string > RequestHeaders { get ; set ; } = [ ] ;
187+ public Dictionary < string , string > ResponseHeaders { get ; set ; } = [ ] ;
188+ }
189+ }
0 commit comments