From 847334eaeaaf25ae87900fe70c47d56e8faf9f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Thu, 23 Jan 2025 13:31:26 +0000 Subject: [PATCH 1/4] Makefile: add sdksanity rule for testing on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To allow compile-testing the C# code on Linux reintroduce the 'sdksanity' rule which got dropped accidentally. Also run the tests. Fixes: e6afe15bf ("Removed erroneously ported recipe.") Signed-off-by: Edwin Török --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index dde13fc24a..e3139f311a 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 From a8d8c2cf782979cf33f74643d1b8374a1c5a1903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Fri, 12 Apr 2024 15:05:04 +0100 Subject: [PATCH 2/4] CP-44752: SDK(C#): Conditionally propagate W3C traceparent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is not available on .Net 4.5. Could use .Net Standard 2.0, .Net Framework 4.6.2 and .NET 8.0 as target, which matches the availability of the System.Diagnostics.DiagnosticSource builtin (and Nuget package for older versions): ``` NET462_OR_GREATER || NETSTANDARD2_0_OR_GREATER || NET8_0_OR_GREATER ``` However there are some bugs in the CI where the xenserver-samples XenSdkSample ends up in a zip, but without its dependency (the DiagnosticSource.dll). So for now just enable this on .Net8, where hopefully this package is part of the runtime already (although of course you'll still need to install the runtime, which for some reason you didn't have to do with .Net6, perhaps .Net6 would come preinstalled on Debian12?). Signed-off-by: Edwin Török --- ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs b/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs index 519cc430d4..5237b1870a 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; @@ -293,6 +296,23 @@ 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); From 81c933f3af1e9a2a3a033b64bd3a1655b8139b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Tue, 9 Apr 2024 18:57:00 +0100 Subject: [PATCH 3/4] CP-44752: SDK(C#): Conditional activity source for JsonRPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Doesn't exist on .Net45. Only creates these activity sources if a listener has been created by the caller, otherwise `activity` will be `null`, and the code would be a no-op by default. A listener is created by OpenTelemetry instrumentation for example. Signed-off-by: Edwin Török --- ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs | 101 +++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs b/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs index 5237b1870a..0af17d5a3f 100644 --- a/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs +++ b/ocaml/sdk-gen/csharp/autogen/src/JsonRpc.cs @@ -158,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; @@ -210,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 @@ -236,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 } } @@ -319,6 +399,15 @@ protected virtual void PerformPostRequest(Stream postStream, Stream responseStre 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 { @@ -346,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 { From 6c44e4cbf1d7fc2092e5350339be27d8baddba10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edwin=20T=C3=B6r=C3=B6k?= Date: Fri, 11 Apr 2025 18:03:20 +0100 Subject: [PATCH 4/4] CP-44752: SDK(C#): add .Net 8.0 as target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently this has fewer dependencies than netstandard2.0, and some projects would've picked net45 in favour of netstandard2.0. Eventually we may want to drop net45, for now just add the new target. There is also net9.0, but it has an EOL date ahead of net8.0 which is LTS. A quick sanity test that the net80 target includes tracing support: ``` make sdksanity strings -el ./_build/default/ocaml/sdk-gen/csharp/autogen-out/src/bin/Release/net80/XenServer.dll|grep traceparent traceparent ``` Signed-off-by: Edwin Török --- ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj b/ocaml/sdk-gen/csharp/autogen/src/XenServer.csproj index 8f36aba76f..e791817875 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