diff --git a/Makefile b/Makefile index 7f7386bf6b..44892a82bf 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 519cc430d4..a94eadf9b0 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 (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_0_OR_GREATER) + activity?.SetStatus(ActivityStatusCode.Ok); +#endif return res2.Result; default: +#if (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_0_OR_GREATER) + activity?.SetStatus(ActivityStatusCode.Ok); +#endif return res1.Result; } } } } +#if (NET462_OR_GREATER || NETSTANDARD2_0_OR_GREATER) + } +#endif } } @@ -293,12 +376,38 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre webRequest.Headers.Add(header.Key, header.Value); } +#if (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_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 (NET462_OR_GREATER || NETSTANDARD2_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 bf38750914..dca57a1755 100644 --- a/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj +++ b/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj @@ -18,6 +18,7 @@ packageIcon.png git README-NuGet.md + true @@ -30,6 +31,9 @@ + + + True diff --git a/ocaml/sdk-gen/csharp/autogen/src/packages.lock.json b/ocaml/sdk-gen/csharp/autogen/src/packages.lock.json new file mode 100644 index 0000000000..bca624389d --- /dev/null +++ b/ocaml/sdk-gen/csharp/autogen/src/packages.lock.json @@ -0,0 +1,84 @@ +{ + "version": 1, + "dependencies": { + ".NETFramework,Version=v4.5": { + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.2, )", + "resolved": "1.0.2", + "contentHash": "5/cSEVld+px/CuRrbohO/djfg6++eR6zGpy88MgqloXvkj//WXWpFZyu/OpkXPN0u5m+dN/EVwLNYFUxD4h2+A==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net45": "1.0.2" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Microsoft.NETFramework.ReferenceAssemblies.net45": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "Nm14pRmqB+4u2JEMdtngnbDcJidTmswMxOJ992TpTwiwcUTERxLlHwwSh0HiUoRjS0TO0sozsiB0h6FHjCUdEA==" + } + }, + ".NETStandard,Version=v2.0": { + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "vaoWjvkG1aenR2XdjaVivlCV9fADfgyhW5bZtXT23qaEea0lWiUljdQuze4E31vKM7ZWJaSUsbYIKE3rnzfZUg==", + "dependencies": { + "System.Memory": "4.5.5", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "dependencies": { + "System.Buffers": "4.5.1", + "System.Numerics.Vectors": "4.4.0", + "System.Runtime.CompilerServices.Unsafe": "4.5.3" + } + }, + "System.Numerics.Vectors": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + } + } + } +} \ No newline at end of file