diff --git a/Makefile b/Makefile index dde13fc24a6..e3139f311a0 100644 --- a/Makefile +++ b/Makefile @@ -83,6 +83,11 @@ doc: sdk: dune build --profile=$(PROFILE) @sdkgen xapi-sdk.install @ocaml/sdk-gen/install +# workaround for no .resx generation, just for compilation testing +sdksanity: sdk + cd _build/install/default/share/csharp/src && dotnet add package Newtonsoft.Json && dotnet build -f netstandard2.0 + cd _build/install/default/share/csharp && dotnet test XenServerTest -p:DefineConstants=BUILD_FOR_TEST + .PHONY: sdk-build-c sdk-build-c: sdk diff --git a/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs b/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs index 519cc430d4e..0af17d5a3f4 100644 --- a/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs +++ b/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs @@ -29,6 +29,9 @@ using System; using System.Collections.Generic; +#if (NET8_0_OR_GREATER) +using System.Diagnostics; +#endif using System.IO; using System.Net; using System.Net.Security; @@ -155,6 +158,41 @@ public partial class JsonRpcClient { private int _globalId; +#if (NET8_0_OR_GREATER) + private static readonly Type ClassType = typeof(JsonRpcClient); + private static readonly System.Reflection.AssemblyName ClassAssemblyName= ClassType?.Assembly?.GetName(); + private static readonly ActivitySource source = new ActivitySource(ClassAssemblyName.Name + "." + ClassType?.FullName, ClassAssemblyName.Version?.ToString()); + + // Follow naming conventions from OpenTelemetry.SemanticConventions + // Not yet on NuGet though: + // dotnet add package OpenTelemetry.SemanticConventions + private static class RpcAttributes { + public const string AttributeRpcMethod = "rpc.method"; + public const string AttributeRpcSystem = "rpc.system"; + public const string AttributeRpcService = "rpc.service"; + public const string AttributeRpcJsonrpcErrorCode = "rpc.jsonrpc.error_code"; + public const string AttributeRpcJsonrpcErrorMessage = "rpc.jsonrpc.error_message"; + public const string AttributeRpcJsonrpcRequestId = "rpc.jsonrpc.request_id"; + public const string AttributeRpcJsonrpcVersion = "rpc.jsonrpc.version"; + + public const string AttributeRpcMessageType = "rpc.message.type"; + public static class RpcMessageTypeValues + { + public const string Sent = "SENT"; + + public const string Received = "RECEIVED"; + } + } + + private static class ServerAttributes { + public const string AttributeServerAddress = "server.address"; + } + + // not part of the SemanticConventions package + private const string ValueJsonRpc = "jsonrpc"; + private const string EventRpcMessage = "rpc.message"; +#endif + public JsonRpcClient(string baseUrl) { Url = baseUrl; @@ -207,6 +245,21 @@ protected virtual T Rpc(string callName, JToken parameters, JsonSerializer se // therefore the latter will be done only in DEBUG mode using (var postStream = new MemoryStream()) { +#if (NET8_0_OR_GREATER) + // the semantic convention is $package.$service/$method + using (Activity activity = source.CreateActivity("XenAPI/" + callName, ActivityKind.Client)) + { + // .NET 5 would use W3C format for the header by default but we build for .Net 4.x still + activity?.SetIdFormat(ActivityIdFormat.W3C); + activity?.Start(); + // Set the fields described in the OpenTelemetry Semantic Conventions: + // https://web.archive.org/web/20250119181511/https://opentelemetry.io/docs/specs/semconv/rpc/json-rpc/ + // https://web.archive.org/web/20241113162246/https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/ + activity?.SetTag(RpcAttributes.AttributeRpcSystem, ValueJsonRpc); + activity?.SetTag(ServerAttributes.AttributeServerAddress, new Uri(Url).Host); + activity?.SetTag(RpcAttributes.AttributeRpcMethod, callName); + activity?.SetTag(RpcAttributes.AttributeRpcJsonrpcRequestId, id.ToString()); +#endif using (var sw = new StreamWriter(postStream)) { #if DEBUG @@ -233,37 +286,67 @@ protected virtual T Rpc(string callName, JToken parameters, JsonSerializer se switch (JsonRpcVersion) { case JsonRpcVersion.v2: +#if (NET8_0_OR_GREATER) + activity?.SetTag(RpcAttributes.AttributeRpcJsonrpcVersion, "2.0"); +#endif #if DEBUG string json2 = responseReader.ReadToEnd(); var res2 = JsonConvert.DeserializeObject>(json2, settings); #else var res2 = (JsonResponseV2)serializer.Deserialize(responseReader, typeof(JsonResponseV2)); #endif + if (res2.Error != null) { var descr = new List { res2.Error.Message }; descr.AddRange(res2.Error.Data.ToObject()); +#if (NET8_0_OR_GREATER) + activity?.SetTag(RpcAttributes.AttributeRpcJsonrpcErrorCode, res2.Error.Code); + activity?.SetTag(RpcAttributes.AttributeRpcJsonrpcErrorMessage, descr); + activity?.SetStatus(ActivityStatusCode.Error); +#endif throw new Failure(descr); } + +#if (NET8_0_OR_GREATER) + activity?.SetStatus(ActivityStatusCode.Ok); +#endif return res2.Result; default: +#if (NET8_0_OR_GREATER) + activity?.SetTag(RpcAttributes.AttributeRpcJsonrpcVersion, "1.0"); +#endif #if DEBUG string json1 = responseReader.ReadToEnd(); var res1 = JsonConvert.DeserializeObject>(json1, settings); #else var res1 = (JsonResponseV1)serializer.Deserialize(responseReader, typeof(JsonResponseV1)); #endif + if (res1.Error != null) { var errorArray = res1.Error.ToObject(); - if (errorArray != null) + if (errorArray != null) { +#if (NET8_0_OR_GREATER) + activity?.SetStatus(ActivityStatusCode.Error); + // we can't be sure whether we'll have a Code here + // the exact format of an error object is not specified in JSONRPC v1 + activity?.SetTag(RpcAttributes.AttributeRpcJsonrpcErrorMessage, errorArray.ToString()); +#endif throw new Failure(errorArray); + } } +#if (NET8_0_OR_GREATER) + activity?.SetStatus(ActivityStatusCode.Ok); +#endif return res1.Result; } } } } +#if (NET8_0_OR_GREATER) + } +#endif } } @@ -293,12 +376,38 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre webRequest.Headers.Add(header.Key, header.Value); } +#if (NET8_0_OR_GREATER) + // propagate W3C traceparent and tracestate + // HttpClient would do this automatically on .NET 5, + // and .NET 6 would provide even more control over this: https://blog.ladeak.net/posts/opentelemetry-net6-httpclient + // the caller must ensure that the activity is in W3C format (by inheritance or direct setting) + var activity = Activity.Current; + if (activity != null && activity.IdFormat == ActivityIdFormat.W3C) + { + webRequest.Headers.Add("traceparent", activity.Id); + var state = activity.TraceStateString; + if (state?.Length > 0) + { + webRequest.Headers.Add("tracestate", state); + } + } +#endif + using (var str = webRequest.GetRequestStream()) { postStream.CopyTo(str); str.Flush(); } +#if (NET8_0_OR_GREATER) + if (activity != null) { + var tags = new ActivityTagsCollection{ + { RpcAttributes.AttributeRpcMessageType, RpcAttributes.RpcMessageTypeValues.Sent } + }; + activity.AddEvent(new ActivityEvent(EventRpcMessage, DateTimeOffset.Now, tags)); + } +#endif + HttpWebResponse webResponse = null; try { @@ -326,6 +435,16 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre str.CopyTo(responseStream); responseStream.Flush(); } + +#if (NET8_0_OR_GREATER) + if (activity != null) { + var tags = new ActivityTagsCollection{ + { RpcAttributes.AttributeRpcMessageType, RpcAttributes.RpcMessageTypeValues.Received } + }; + activity.AddEvent(new ActivityEvent(EventRpcMessage, DateTimeOffset.Now, tags)); + } +#endif + } finally { diff --git a/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj b/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj index 8f36aba76fa..e7918178753 100644 --- a/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj +++ b/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj @@ -1,7 +1,7 @@  0.0.0 - netstandard2.0;net45 + net80;netstandard2.0;net45 Library XenAPI True