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
+
+
+
+
+
+