32
32
using System . IO ;
33
33
using System . Net ;
34
34
#if ( NET8_0_OR_GREATER )
35
+ using System . Diagnostics ;
35
36
using System . Linq ;
36
37
using System . Net . Http ;
37
38
using System . Net . Http . Headers ;
@@ -71,6 +72,8 @@ public static JsonRequest Create(JsonRpcVersion jsonRpcVersion, int id, string m
71
72
}
72
73
}
73
74
75
+ public abstract string JsonRPC { get ; }
76
+
74
77
/// <summary>
75
78
/// Unique call id. Can be null in JSON_RPC v2.0, but xapi disallows it.
76
79
/// </summary>
@@ -101,6 +104,9 @@ public JsonRequestV1(int id, string method, JToken parameters)
101
104
: base ( id , method , parameters )
102
105
{
103
106
}
107
+
108
+ [ JsonIgnore ]
109
+ public override string JsonRPC => "1.0" ;
104
110
}
105
111
106
112
internal class JsonRequestV2 : JsonRequest
@@ -111,7 +117,7 @@ public JsonRequestV2(int id, string method, JToken parameters)
111
117
}
112
118
113
119
[ JsonProperty ( "jsonrpc" , Required = Required . Always ) ]
114
- public string JsonRPC => "2.0" ;
120
+ public override string JsonRPC => "2.0" ;
115
121
}
116
122
117
123
@@ -158,6 +164,42 @@ public partial class JsonRpcClient
158
164
{
159
165
private int _globalId ;
160
166
167
+ #if ( NET8_0_OR_GREATER )
168
+ private static readonly Type ClassType = typeof ( JsonRpcClient ) ;
169
+ private static readonly System . Reflection . AssemblyName ClassAssemblyName = ClassType ? . Assembly ? . GetName ( ) ;
170
+ private static readonly ActivitySource source = new ActivitySource ( ClassAssemblyName . Name + "." + ClassType ? . FullName , ClassAssemblyName . Version ? . ToString ( ) ) ;
171
+
172
+ // Follow naming conventions from OpenTelemetry.SemanticConventions
173
+ // Not yet on NuGet though:
174
+ // dotnet add package OpenTelemetry.SemanticConventions
175
+ private static class RpcAttributes
176
+ {
177
+ public const string AttributeRpcMethod = "rpc.method" ;
178
+ public const string AttributeRpcSystem = "rpc.system" ;
179
+ public const string AttributeRpcService = "rpc.service" ;
180
+ public const string AttributeRpcJsonrpcErrorCode = "rpc.jsonrpc.error_code" ;
181
+ public const string AttributeRpcJsonrpcErrorMessage = "rpc.jsonrpc.error_message" ;
182
+ public const string AttributeRpcJsonrpcRequestId = "rpc.jsonrpc.request_id" ;
183
+ public const string AttributeRpcJsonrpcVersion = "rpc.jsonrpc.version" ;
184
+ public const string AttributeRpcMessageType = "rpc.message.type" ;
185
+
186
+ public static class RpcMessageTypeValues
187
+ {
188
+ public const string Sent = "SENT" ;
189
+ public const string Received = "RECEIVED" ;
190
+ }
191
+ }
192
+
193
+ private static class ServerAttributes
194
+ {
195
+ public const string AttributeServerAddress = "server.address" ;
196
+ }
197
+
198
+ // not part of the SemanticConventions package
199
+ private const string ValueJsonRpc = "jsonrpc" ;
200
+ private const string EventRpcMessage = "rpc.message" ;
201
+ #endif
202
+
161
203
public JsonRpcClient ( string baseUrl )
162
204
{
163
205
Url = baseUrl ;
@@ -216,63 +258,98 @@ protected virtual T Rpc<T>(string callName, JToken parameters, JsonSerializer se
216
258
// therefore the latter will be done only in DEBUG mode
217
259
using ( var postStream = new MemoryStream ( ) )
218
260
{
219
- using ( var sw = new StreamWriter ( postStream ) )
261
+ #if ( NET8_0_OR_GREATER )
262
+ // the semantic convention is $package.$service/$method
263
+ using ( Activity activity = source . CreateActivity ( "XenAPI/" + callName , ActivityKind . Client ) )
220
264
{
265
+ activity ? . Start ( ) ;
266
+ // Set the fields described in the OpenTelemetry Semantic Conventions:
267
+ // https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/
268
+ // https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/
269
+ activity ? . SetTag ( RpcAttributes . AttributeRpcSystem , ValueJsonRpc ) ;
270
+ activity ? . SetTag ( ServerAttributes . AttributeServerAddress , new Uri ( Url ) . Host ) ;
271
+ activity ? . SetTag ( RpcAttributes . AttributeRpcMethod , callName ) ;
272
+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcRequestId , id . ToString ( ) ) ;
273
+ #endif
274
+ using ( var sw = new StreamWriter ( postStream ) )
275
+ {
221
276
#if DEBUG
222
- var settings = CreateSettings ( serializer . Converters ) ;
223
- string jsonReq = JsonConvert . SerializeObject ( request , settings ) ;
224
- if ( RequestEvent != null )
225
- RequestEvent ( jsonReq ) ;
226
- sw . Write ( jsonReq ) ;
277
+ var settings = CreateSettings ( serializer . Converters ) ;
278
+ string jsonReq = JsonConvert . SerializeObject ( request , settings ) ;
279
+ if ( RequestEvent != null )
280
+ RequestEvent ( jsonReq ) ;
281
+ sw . Write ( jsonReq ) ;
227
282
#else
228
- if ( RequestEvent != null )
229
- RequestEvent ( callName ) ;
230
- serializer . Serialize ( sw , request ) ;
283
+ if ( RequestEvent != null )
284
+ RequestEvent ( callName ) ;
285
+ serializer . Serialize ( sw , request ) ;
231
286
#endif
232
- sw . Flush ( ) ;
233
- postStream . Seek ( 0 , SeekOrigin . Begin ) ;
287
+ sw . Flush ( ) ;
288
+ postStream . Seek ( 0 , SeekOrigin . Begin ) ;
234
289
235
- using ( var responseStream = new MemoryStream ( ) )
236
- {
237
- PerformPostRequest ( postStream , responseStream ) ;
238
- responseStream . Position = 0 ;
239
-
240
- using ( var responseReader = new StreamReader ( responseStream ) )
290
+ using ( var responseStream = new MemoryStream ( ) )
241
291
{
242
- switch ( JsonRpcVersion )
292
+ PerformPostRequest ( postStream , responseStream ) ;
293
+ responseStream . Position = 0 ;
294
+
295
+ using ( var responseReader = new StreamReader ( responseStream ) )
243
296
{
244
- case JsonRpcVersion . v2 :
297
+ #if ( NET8_0_OR_GREATER )
298
+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcVersion , request . JsonRPC ) ;
299
+ #endif
300
+ switch ( JsonRpcVersion )
301
+ {
302
+ case JsonRpcVersion . v2 :
245
303
#if DEBUG
246
- string json2 = responseReader . ReadToEnd ( ) ;
247
- var res2 = JsonConvert . DeserializeObject < JsonResponseV2 < T > > ( json2 , settings ) ;
304
+ string json2 = responseReader . ReadToEnd ( ) ;
305
+ var res2 = JsonConvert . DeserializeObject < JsonResponseV2 < T > > ( json2 , settings ) ;
248
306
#else
249
- var res2 = ( JsonResponseV2 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV2 < T > ) ) ;
307
+ var res2 = ( JsonResponseV2 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV2 < T > ) ) ;
250
308
#endif
251
- if ( res2 . Error != null )
252
- {
253
- var descr = new List < string > { res2 . Error . Message } ;
254
- descr . AddRange ( res2 . Error . Data . ToObject < string [ ] > ( ) ) ;
255
- throw new Failure ( descr ) ;
256
- }
257
- return res2 . Result ;
258
- default :
309
+ if ( res2 . Error != null )
310
+ {
311
+ var descr = new List < string > { res2 . Error . Message } ;
312
+ descr . AddRange ( res2 . Error . Data . ToObject < string [ ] > ( ) ) ;
313
+ #if ( NET8_0_OR_GREATER )
314
+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcErrorCode , res2 . Error . Code ) ;
315
+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcErrorMessage , descr ) ;
316
+ activity ? . SetStatus ( ActivityStatusCode . Error ) ;
317
+ #endif
318
+ throw new Failure ( descr ) ;
319
+ }
320
+ #if ( NET8_0_OR_GREATER )
321
+ activity ? . SetStatus ( ActivityStatusCode . Ok ) ;
322
+ #endif
323
+ return res2 . Result ;
324
+ default :
259
325
#if DEBUG
260
- string json1 = responseReader . ReadToEnd ( ) ;
261
- var res1 = JsonConvert . DeserializeObject < JsonResponseV1 < T > > ( json1 , settings ) ;
326
+ string json1 = responseReader . ReadToEnd ( ) ;
327
+ var res1 = JsonConvert . DeserializeObject < JsonResponseV1 < T > > ( json1 , settings ) ;
262
328
#else
263
- var res1 = ( JsonResponseV1 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV1 < T > ) ) ;
329
+ var res1 = ( JsonResponseV1 < T > ) serializer . Deserialize ( responseReader , typeof ( JsonResponseV1 < T > ) ) ;
264
330
#endif
265
- if ( res1 . Error != null )
266
- {
267
- var errorArray = res1 . Error . ToObject < string [ ] > ( ) ;
331
+ var errorArray = res1 . Error ? . ToObject < string [ ] > ( ) ;
268
332
if ( errorArray != null )
333
+ {
334
+ #if ( NET8_0_OR_GREATER )
335
+ activity ? . SetStatus ( ActivityStatusCode . Error ) ;
336
+ // we can't be sure whether we'll have a Code here
337
+ // the exact format of an error object is not specified in JSONRPC v1
338
+ activity ? . SetTag ( RpcAttributes . AttributeRpcJsonrpcErrorMessage , errorArray . ToString ( ) ) ;
339
+ #endif
269
340
throw new Failure ( errorArray ) ;
270
- }
271
- return res1 . Result ;
341
+ }
342
+ #if ( NET8_0_OR_GREATER )
343
+ activity ? . SetStatus ( ActivityStatusCode . Ok ) ;
344
+ #endif
345
+ return res1 . Result ;
346
+ }
272
347
}
273
348
}
274
349
}
350
+ #if ( NET8_0_OR_GREATER )
275
351
}
352
+ #endif
276
353
}
277
354
}
278
355
@@ -294,7 +371,7 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre
294
371
Proxy = WebProxy
295
372
} ;
296
373
297
- httpHandler . ServerCertificateCustomValidationCallback = ServerCertificateValidationCallback ;
374
+ httpHandler . ServerCertificateCustomValidationCallback = ServerCertificateValidationCallback ;
298
375
299
376
httpClient = new HttpClient ( httpHandler ) { Timeout = TimeSpan . FromMilliseconds ( Timeout ) } ;
300
377
@@ -313,6 +390,26 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre
313
390
requestMessage . Headers . Add ( header . Key , header . Value ) ;
314
391
}
315
392
393
+ // propagate W3C traceparent and tracestate
394
+ // HttpClient would do this automatically on .NET 5,
395
+ // and .NET 6 would provide even more control over this: https://blog.ladeak.net/posts/opentelemetry-net6-httpclient
396
+ // the caller must ensure that the activity is in W3C format (by inheritance or direct setting)
397
+ var activity = Activity . Current ;
398
+ if ( activity != null )
399
+ {
400
+ if ( activity . IdFormat == ActivityIdFormat . W3C )
401
+ {
402
+ requestMessage . Headers . Add ( "traceparent" , activity . Id ) ;
403
+ var state = activity . TraceStateString ;
404
+
405
+ if ( state ? . Length > 0 )
406
+ requestMessage . Headers . Add ( "tracestate" , state ) ;
407
+ }
408
+
409
+ var tags = new ActivityTagsCollection { { RpcAttributes . AttributeRpcMessageType , RpcAttributes . RpcMessageTypeValues . Sent } } ;
410
+ activity . AddEvent ( new ActivityEvent ( EventRpcMessage , DateTimeOffset . Now , tags ) ) ;
411
+ }
412
+
316
413
responseMessage = httpClient . SendAsync ( requestMessage ) . Result ;
317
414
responseMessage . EnsureSuccessStatusCode ( ) ;
318
415
@@ -321,10 +418,16 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre
321
418
responseStream . Flush ( ) ;
322
419
323
420
ResponseHeaders = responseMessage . Headers . ToDictionary ( header => header . Key , header => string . Join ( "," , header . Value ) ) ;
421
+
422
+ if ( activity != null )
423
+ {
424
+ var tags = new ActivityTagsCollection { { RpcAttributes . AttributeRpcMessageType , RpcAttributes . RpcMessageTypeValues . Received } } ;
425
+ activity . AddEvent ( new ActivityEvent ( EventRpcMessage , DateTimeOffset . Now , tags ) ) ;
426
+ }
324
427
}
325
428
finally
326
429
{
327
- RequestHeaders = null ;
430
+ RequestHeaders = null ;
328
431
responseMessage ? . Dispose ( ) ;
329
432
requestMessage ? . Dispose ( ) ;
330
433
httpClient ? . Dispose ( ) ;
0 commit comments