diff --git a/.config/mise/tasks/semconv/check b/.config/mise/tasks/semconv/check new file mode 100755 index 0000000000..55edb693fe --- /dev/null +++ b/.config/mise/tasks/semconv/check @@ -0,0 +1,12 @@ +#!/bin/sh +#MISE description="Validate the semantic convention registry and generated constants" + +set -eu + +root=$(CDPATH='' cd -- "$(dirname -- "$0")/../../../.." && pwd) +expected="$root/src/TrogonEventStore.SemanticConventions/Generated" +generated=$(mktemp -d) +trap 'rm -rf "$generated"' EXIT HUP INT TERM + +"$root/.config/mise/tasks/semconv/generate" "$generated" +diff -ru "$expected" "$generated" diff --git a/.config/mise/tasks/semconv/generate b/.config/mise/tasks/semconv/generate new file mode 100755 index 0000000000..f808e5479e --- /dev/null +++ b/.config/mise/tasks/semconv/generate @@ -0,0 +1,35 @@ +#!/bin/sh +#MISE description="Generate the reusable semantic convention constants" + +set -eu + +root=$(CDPATH='' cd -- "$(dirname -- "$0")/../../../.." && pwd) +output=${1:-"$root/src/TrogonEventStore.SemanticConventions/Generated"} +registry_version=$(sed -n '1p' "$root/otel/semconv/registry-version") +registry="https://github.com/open-telemetry/semantic-conventions@${registry_version}[model]" +staging=$(mktemp -d) +trap 'rm -rf "$staging"' EXIT HUP INT TERM + +weaver registry check \ + --future \ + --registry "$registry" + +weaver registry generate csharp "$staging" \ + --future \ + --registry "$registry" \ + --templates "$root/otel/semconv/templates" + +set -- "$staging"/*.g.cs +[ -e "$1" ] || exit 1 + +mkdir -p "$output" +for generated_file in "$output"/*.g.cs +do + [ -e "$generated_file" ] || break + rm "$generated_file" +done + +for generated_file +do + mv "$generated_file" "$output/" +done diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 652df3b2da..fdfac81038 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -27,6 +27,21 @@ permissions: packages: read jobs: + semantic-conventions: + runs-on: ubuntu-latest + name: Semantic Conventions + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Install repository tools + uses: jdx/mise-action@v4.2.1 + with: + cache: false + - name: Verify semantic conventions + run: mise run semconv:check + csharp-quality: runs-on: ubuntu-latest name: C# Quality @@ -66,6 +81,10 @@ jobs: - name: Verify C# formatting run: | ./ci/csharp-format-changed.sh src/EventStore.sln "${{ steps.refs.outputs.base }}" "${{ steps.refs.outputs.head }}" + - name: Verify semantic convention package + run: | + dotnet pack --no-restore --configuration Release --output artifacts/packages \ + src/TrogonEventStore.SemanticConventions/TrogonEventStore.SemanticConventions.csproj vulnerability-scan: runs-on: ubuntu-latest diff --git a/mise.toml b/mise.toml index eb20a3808a..b014056189 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,6 @@ +[tools] +"github:open-telemetry/weaver" = "0.24.2" + [tasks."ui:dev"] description = "Run a local file-backed node for Razor UI testing" run = ''' diff --git a/otel/semconv/registry-version b/otel/semconv/registry-version new file mode 100644 index 0000000000..518f2eaf18 --- /dev/null +++ b/otel/semconv/registry-version @@ -0,0 +1 @@ +v1.43.0 diff --git a/otel/semconv/templates/registry/csharp/attribute-names.cs.j2 b/otel/semconv/templates/registry/csharp/attribute-names.cs.j2 new file mode 100644 index 0000000000..779f7c36e6 --- /dev/null +++ b/otel/semconv/templates/registry/csharp/attribute-names.cs.j2 @@ -0,0 +1,13 @@ +// + +namespace TrogonEventStore.SemanticConventions +{ + public static class AttributeNames + { +{% for group in ctx %} +{% for attribute in group.attributes %} + public const string {{ attribute.name | pascal_case }} = "{{ attribute.name }}"; +{% endfor %} +{% endfor %} + } +}{{ "\n" }} diff --git a/otel/semconv/templates/registry/csharp/weaver.yaml b/otel/semconv/templates/registry/csharp/weaver.yaml new file mode 100644 index 0000000000..416da1351b --- /dev/null +++ b/otel/semconv/templates/registry/csharp/weaver.yaml @@ -0,0 +1,19 @@ +whitespace_control: + trim_blocks: true + lstrip_blocks: true + +templates: + - template: attribute-names.cs.j2 + filter: > + semconv_grouped_attributes + | map({ + root_namespace: .root_namespace, + attributes: [.attributes[] | select( + .name == "service.name" or + .name == "service.instance.id" or + .name == "service.version" + )] + }) + | map(select(.attributes | length > 0)) + application_mode: single + file_name: AttributeNames.g.cs diff --git a/src/EventStore.Core/EventStore.Core.csproj b/src/EventStore.Core/EventStore.Core.csproj index aa1bc4a9e6..b8195ca616 100644 --- a/src/EventStore.Core/EventStore.Core.csproj +++ b/src/EventStore.Core/EventStore.Core.csproj @@ -47,6 +47,7 @@ + diff --git a/src/EventStore.Core/Log/OpenTelemetryLogger.cs b/src/EventStore.Core/Log/OpenTelemetryLogger.cs index ee02febb19..fd005c02bc 100644 --- a/src/EventStore.Core/Log/OpenTelemetryLogger.cs +++ b/src/EventStore.Core/Log/OpenTelemetryLogger.cs @@ -8,6 +8,7 @@ using Serilog; using Serilog.Filters; using Serilog.Sinks.OpenTelemetry; +using TrogonEventStore.SemanticConventions; namespace EventStore.Common.Log; @@ -37,9 +38,9 @@ public static LoggerConfiguration AddOpenTelemetryLogger( { options.ResourceAttributes = new Dictionary { - ["service.name"] = "eventstore", - ["service.instance.id"] = componentName, - ["service.version"] = VersionInfo.Version + [AttributeNames.ServiceName] = "eventstore", + [AttributeNames.ServiceInstanceId] = componentName, + [AttributeNames.ServiceVersion] = VersionInfo.Version }; options.Protocol = otlpExporterConfig.Protocol switch { diff --git a/src/EventStore.sln b/src/EventStore.sln index 78dd9431e7..ef6553395d 100644 --- a/src/EventStore.sln +++ b/src/EventStore.sln @@ -63,6 +63,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventStore.SystemRuntime.Te EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CB56B2BD-5ABA-49C7-BD57-21B9CD5C3205}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrogonEventStore.SemanticConventions", "TrogonEventStore.SemanticConventions\TrogonEventStore.SemanticConventions.csproj", "{05D9513D-A26A-410A-A145-4A17548EB79C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -349,6 +351,18 @@ Global {CA966906-51C8-4520-924E-988FA396B712}.Release|ARM64.Build.0 = Release|ARM64 {CA966906-51C8-4520-924E-988FA396B712}.Release|x64.ActiveCfg = Release|x64 {CA966906-51C8-4520-924E-988FA396B712}.Release|x64.Build.0 = Release|x64 + {05D9513D-A26A-410A-A145-4A17548EB79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Debug|ARM64.Build.0 = Debug|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Debug|x64.ActiveCfg = Debug|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Debug|x64.Build.0 = Debug|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Release|Any CPU.Build.0 = Release|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Release|ARM64.ActiveCfg = Release|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Release|ARM64.Build.0 = Release|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Release|x64.ActiveCfg = Release|Any CPU + {05D9513D-A26A-410A-A145-4A17548EB79C}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/TrogonEventStore.SemanticConventions/Generated/AttributeNames.g.cs b/src/TrogonEventStore.SemanticConventions/Generated/AttributeNames.g.cs new file mode 100644 index 0000000000..a51835e3dd --- /dev/null +++ b/src/TrogonEventStore.SemanticConventions/Generated/AttributeNames.g.cs @@ -0,0 +1,11 @@ +// + +namespace TrogonEventStore.SemanticConventions +{ + public static class AttributeNames + { + public const string ServiceInstanceId = "service.instance.id"; + public const string ServiceName = "service.name"; + public const string ServiceVersion = "service.version"; + } +} diff --git a/src/TrogonEventStore.SemanticConventions/README.md b/src/TrogonEventStore.SemanticConventions/README.md new file mode 100644 index 0000000000..31380553ac --- /dev/null +++ b/src/TrogonEventStore.SemanticConventions/README.md @@ -0,0 +1,5 @@ +# TrogonEventStore Semantic Conventions + +This package gives TrogonEventStore components one generated source for OpenTelemetry attribute names. It has no runtime dependencies. + +The pinned OpenTelemetry registry version and C# templates under `otel/semconv` are the source of truth. Regenerate the committed constants with `mise run semconv:generate` and verify them with `mise run semconv:check`. diff --git a/src/TrogonEventStore.SemanticConventions/TrogonEventStore.SemanticConventions.csproj b/src/TrogonEventStore.SemanticConventions/TrogonEventStore.SemanticConventions.csproj new file mode 100644 index 0000000000..daa3e106dc --- /dev/null +++ b/src/TrogonEventStore.SemanticConventions/TrogonEventStore.SemanticConventions.csproj @@ -0,0 +1,25 @@ + + + netstandard2.0 + 0.1.0 + + true + TrogonEventStore.SemanticConventions + TrogonEventStore.SemanticConventions + TrogonEventStore.SemanticConventions + Straw Hat, LLC + Straw Hat, LLC + Copyright Straw Hat, LLC + Shared OpenTelemetry semantic convention constants for TrogonEventStore components. + https://github.com/TrogonStack/TrogonEventStore + README.md + + opentelemetry;semantic-conventions;weaver + https://github.com/TrogonStack/TrogonEventStore + git + + + + + +