Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/skills/prepare-release/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: prepare-release
description: Prepare a new NuGet release of this repository (version bump, release notes, release PR to master). Use this whenever the user mentions releasing, publishing, shipping, cutting a release, bumping the version, updating OpenTelemetry dependencies, or preparing release notes — even if they don't say "release" explicitly. Decides the next SemVer version automatically and proceeds when the shipped library project has dependency updates or there are any new commits since the last tag.
description: Prepare a new NuGet release of this repository (version bump, release notes, release PR to main). Use this whenever the user mentions releasing, publishing, shipping, cutting a release, bumping the version, updating OpenTelemetry dependencies, or preparing release notes — even if they don't say "release" explicitly. Decides the next SemVer version automatically and proceeds when the shipped library project has dependency updates or there are any new commits since the last tag.
---

# Prepare release
Expand Down Expand Up @@ -55,9 +55,9 @@ prepare the repository and open the PR.

12. **Commit** (no tag) — stage csproj(s), docs, release notes; message `release: v<version>`.

13. **Push & PR to master**
13. **Push & PR to main**
- `git push -u origin release/v<version>`
- `gh pr create --base master --head release/v<version> --title "release: v<version>" --body <notes>`
- `gh pr create --base main --head release/v<version> --title "release: v<version>" --body <notes>`

14. **Report** the PR link and remind: after merge, trigger **Deploy Nuget** manually
(Actions → Deploy Nuget → Run workflow) — it tags `v<version>` and publishes.
109 changes: 79 additions & 30 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,38 @@ Tool-specific files (`CLAUDE.md`, `.github/copilot-instructions.md`) point here.
## Project

NuGet package that wires up OpenTelemetry (tracing, metrics, logging) for
ASP.NET Core via a single `AddTelemetry()` call and `appsettings.json`.
.NET applications via a single `AddTelemetry()` call and `appsettings.json`.

## Repository layout

```
src/
OpenTelemetryExtension.Configuration/ # Library (netstandard2.0 + net10.0)
OpenTelemetryExtension.Configuration.Tests/ # xUnit unit tests (net10.0, in-process)
OpenTelemetryExtension.Configuration.IntegrationTests/ # Integration tests (net10.0) — query a live OpenObserve
Directory.Build.props # Shared props: nullable, implicit usings, warnings-as-errors, style enforced in build
OpenTelemetryExtension.Configuration/ # Library (netstandard2.0 + net10.0) — the shipped NuGet package
OpenTelemetryExtension.Configuration.Tests/ # xUnit unit tests (net10.0, in-process, Category=Unit)
OpenTelemetryExtension.Configuration.IntegrationTests/ # Integration tests (net10.0, Category=Integration) — query a live OpenObserve
OpenTelemetryExtension.Configuration.Sample.WebApi/ # ASP.NET Core sample app (net10.0)
OpenTelemetryExtension.Configuration.Sample.Wpf/ # WPF desktop sample app (net10.0-windows)
OpenTelemetryExtension.slnx # Solution file
infrastructure/helm/ # Helm charts + install scripts (OpenObserve, SQL Server, Aspire Dashboard, SigNoz)
.github/workflows/
ci.yml # Build + test + coverage on push
deploy-nuget.yml # Manual NuGet publish + GitHub Release
ci.yml # Build + unit tests (Category=Unit) + coverage → Coveralls
deploy-nuget.yml # Manual NuGet publish + git tag + GitHub Release
release-notes/ # v{VERSION}.md per release
OpenTelemetryExtension.slnx # Solution file (repo root)
```

## Branches & CI

- `develop` is the default working branch; `main` receives release PRs only.
- `ci.yml` runs on push/PR to `develop` and `main` (Windows runner): build,
unit tests with `--filter "Category=Unit"`, coverage via Coverlet +
ReportGenerator, upload to Coveralls.
- `deploy-nuget.yml` is **manual only** (`workflow_dispatch`, Linux runner): it
builds *only* the library and unit-test projects (the WPF sample cannot build
on Linux), runs the unit tests, packs, publishes to NuGet.org and GitHub
Packages, tags `v{VERSION}` and creates a GitHub Release from
`release-notes/v{VERSION}.md`.

## Public API (2 classes, minimal surface)

```csharp
Expand All @@ -35,25 +49,49 @@ services.AddTelemetry(configuration, o => { ... }, "Sec"); // combined + custom
services.AddTelemetry(o => { o.Endpoint = new Uri("..."); });
```

`TelemetryOptions` is the single configuration model. `Enabled` defaults to
`true`; set it to `false` to make `AddTelemetry()` a no-op. `Endpoint` is
`[Required]` and validated at registration time when `Enabled = true`. The
configuration section name (`Telemetry`) is overridable via the `sectionName`
parameter on the `IConfiguration` overloads.
`TelemetryOptions` is the single configuration model:

- **Connection**: `Endpoint` (`[Required]`, validated at registration when
enabled), `Headers`, `Protocol` (default `HttpProtobuf`)
- **Identity**: `ServiceName`, `ResourceAttributes`
- **Signal switches**: `Enabled` (default `true`; `false` makes `AddTelemetry()`
a no-op), `EnableTracing`, `EnableMetrics`, `EnableLogging`
- **Instrumentation switches** (all default `true`):
`EnableAspNetCoreInstrumentation`, `EnableHttpClientInstrumentation`,
`EnableRuntimeInstrumentation`, plus `RecordExceptions`, `ExcludedPaths`
(default `["/health"]`)
- **Tracing/metrics extension points**: `AdditionalTracingSources`,
`AdditionalMeters`, `SampleRatio`, and `ConfigureTracing` /
`ConfigureMetrics` / `ConfigureLogging` callbacks
- **Logging**: `IncludeScopes`, `IncludeFormattedMessage`

The configuration section name (`Telemetry`) is overridable via the
`sectionName` parameter on the `IConfiguration` overloads.

## Dependencies

- Main library: only OpenTelemetry SDK packages
(`OpenTelemetry.Exporter.OpenTelemetryProtocol`,
`OpenTelemetry.Extensions.Hosting`, `OpenTelemetry.Instrumentation.Http`,
`OpenTelemetry.Instrumentation.Runtime`;
`OpenTelemetry.Instrumentation.AspNetCore` is conditional — **not**
referenced for `netstandard2.0` so non-web clients stay lean). Do not add
other third-party packages.
- Unit tests: xUnit + coverlet. The library has `InternalsVisibleTo`
for the unit-test project.

## Build & test

```bash
dotnet build OpenTelemetryExtension.slnx -c Release
dotnet test src/OpenTelemetryExtension.Configuration.Tests -c Release # unit tests
dotnet test src/OpenTelemetryExtension.Configuration.Tests -c Release --filter "Category=Unit" # unit tests
```

Unit tests use **xUnit + Moq** and run in-process via `ServiceCollection` — no
infrastructure required.
Unit tests run in-process via `ServiceCollection` — no infrastructure required.

**Whenever you add or change a feature, run the unit tests. When the telemetry
stack is running, also run the integration tests** (see below). **CI runs the
unit tests only** (the workflows filter on `Category=Unit`).
unit tests only.**

### Integration tests

Expand All @@ -64,9 +102,15 @@ API to confirm the data was ingested.

- Needs the OpenObserve Helm chart (`infrastructure/helm/helm-install-openobserve.cmd`);
the SQL Server chart (`helm-install-sqlserver.cmd`) is required only for the SQL test.
- Every test is `[Trait("Category", "Integration")]` and **auto-skips** when the
backend (or SQL Server) is unreachable, so the suite stays green without the stack.
- Endpoints/credentials default to the Helm chart values; override via `OTEL_IT_*` env vars.
- Tests use `[IntegrationFact]` / `[SqlIntegrationFact]` (in `Utils/`) instead
of `[Fact]` — they **auto-skip** when OpenObserve (`localhost:30117`) or SQL
Server (`localhost:31433`) is unreachable, so the suite stays green without
the stack.
- Shared helpers live in `Utils/`: `IntegrationConfig` (endpoints/credentials),
`OpenObserveClient` (`_search` queries), `OtelTestHost`, `Reachability`.
- Endpoints/credentials default to the Helm chart values; override via env vars:
`OTEL_IT_OPENOBSERVE_URL`, `OTEL_IT_OPENOBSERVE_USER`,
`OTEL_IT_OPENOBSERVE_PASSWORD`, `OTEL_IT_OTLP_HEADERS`, `OTEL_IT_SQL_CONNECTION`.
- Run: `dotnet test src/OpenTelemetryExtension.Configuration.IntegrationTests -c Release`.

## Language & framework
Expand All @@ -76,14 +120,14 @@ API to confirm the data was ingested.
- Target frameworks: `netstandard2.0` and `net10.0` — guard net5.0+ APIs with
`#if NET5_0_OR_GREATER`. Do not use APIs unavailable on `netstandard2.0`
without the guard.
- No third-party packages in the main library beyond the OpenTelemetry SDK
packages already referenced
- `src/Directory.Build.props` applies to every project: `Nullable`,
`ImplicitUsings`, `LangVersion=latest`, `TreatWarningsAsErrors=true`,
`EnforceCodeStyleInBuild=true` — a style violation fails the build

## Code conventions

- **File-scoped namespaces** required (`namespace Foo;` not `namespace Foo { }`)
- **EditorConfig** is enforced at build time (`EnforceCodeStyleInBuild=True`) —
do not bypass it
- **EditorConfig** is enforced at build time — do not bypass it
- Private fields: `_camelCase`, static fields: `s_camelCase`, interfaces: `IFoo`,
type params: `TFoo`
- `var` for local variables when the type is obvious from the right-hand side
Expand All @@ -95,29 +139,33 @@ API to confirm the data was ingested.

## Tests

- **Every unit test class must carry `[Trait("Category", "Unit")]`** (class
level). CI and the deploy workflow filter on `Category=Unit` — an untagged
test is silently never run in CI.
- Integration test classes carry `[Trait("Category", "Integration")]` and use
`[IntegrationFact]` / `[SqlIntegrationFact]` instead of `[Fact]`
- xUnit `[Fact]` for single cases, `[Theory]` + `[InlineData]` for parameterised
- Method name pattern: `MethodOrProperty_Condition_ExpectedResult`
- Arrange / Act / Assert with a blank line between each section
- Arrange / Act / Assert with a blank line between each section; trivial
single-expression tests (e.g. default-value checks) may stay compact
- Use `ServiceCollection` + `BuildServiceProvider()` to verify DI registrations —
no reflection hacks
- Use `Record.Exception` (not `Assert.Throws<T>`) when asserting that no
exception is thrown
- Do not use `Thread.Sleep` or `Task.Delay` in **unit** tests (integration tests
may poll the backend until telemetry is queryable)
- Integration tests live in the `*.IntegrationTests` project, are marked
`[Trait("Category", "Integration")]` and assert against a live OpenObserve via
its `_search` API — see [Integration tests](#integration-tests)

## Versioning & release

- Version lives in
`src/OpenTelemetryExtension.Configuration/OpenTelemetryExtension.Configuration.csproj`
(`<Version>`)
- Do not change `<Version>` without also creating `release-notes/v{VERSION}.md`
- NuGet publish is **manual** (`workflow_dispatch`) — never triggered
automatically
— the GitHub Release body is taken from that file
- NuGet publish is **manual** (`workflow_dispatch` on `deploy-nuget.yml`) —
never triggered automatically; it also creates the `v{VERSION}` git tag
- The full release-prep workflow (decide SemVer, bump, update deps, build/test,
end-to-end smoke test, release notes, PR to `master`) is encoded in the
end-to-end smoke test, release notes, PR to `main`) is encoded in the
**`prepare-release`** skill at `.claude/skills/prepare-release/`. Run it via
Claude Code (`/prepare-release`) when cutting a release; it only prepares the
PR — publishing stays the manual `deploy-nuget.yml` trigger.
Expand All @@ -130,6 +178,7 @@ API to confirm the data was ingested.
are excluded from code coverage)
- Do not add new public API surface without a corresponding test in
`TelemetryOptionsTests.cs` or `TelemetryServiceCollectionExtensionsTests.cs`
- Do not add test classes without a `Category` trait (see [Tests](#tests))

## Adding a new instrumentation option

Expand Down
14 changes: 12 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
# CLAUDE.md

See [AGENTS.md](./AGENTS.md) for project conventions, build instructions, and
contribution rules. That file is the single source of truth for all AI agents.
[AGENTS.md](./AGENTS.md) is the single source of truth for this repository:
project layout, build & test commands, code conventions, test rules, and the
release process all live there. Read it before making changes — do not
duplicate its content here.

## Claude-specific notes

- Quick start: `dotnet build OpenTelemetryExtension.slnx -c Release`, then
`dotnet test src/OpenTelemetryExtension.Configuration.Tests -c Release --filter "Category=Unit"`.
- Day-to-day work happens on `develop`; release PRs target `main`.
- Use the `/prepare-release` skill (`.claude/skills/prepare-release/`) for the
entire release workflow — never bump `<Version>` or publish manually.
14 changes: 11 additions & 3 deletions OpenTelemetryExtension.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@
<Folder Name="/.github/">
<File Path=".github/copilot-instructions.md" />
</Folder>
<Folder Name="/Release Notes/">
<File Path="release-notes/v1.0.0.md" />
<File Path="release-notes/v1.0.1.md" />
<File Path="release-notes/v1.0.2.md" />
<File Path="release-notes/v1.1.0.md" />
<File Path="release-notes/v1.1.1.md" />
<File Path="release-notes/v2.0.0.md" />
<File Path="release-notes/v2.0.1.md" />
</Folder>
<Folder Name="/.github/workflows/">
<File Path=".github/workflows/ci.yml" />
<File Path=".github/workflows/deploy-nuget.yml" />
Expand All @@ -43,8 +52,8 @@
<File Path="infrastructure/helm/helm-install-openobserve.cmd" />
<File Path="infrastructure/helm/helm-install-signoz.cmd" />
<File Path="infrastructure/helm/helm-install-sqlserver.cmd" />
<File Path="infrastructure/helm/nodeports.aspire-dashboard.yaml" />
<File Path="infrastructure/helm/values.aspire-dashboard.yaml" />
<File Path="infrastructure/helm/aspire-dashboard.nodeports.yaml" />
<File Path="infrastructure/helm/aspire-dashboard.values.yaml" />
</Folder>
<Folder Name="/Infrastructure/Helm/chart-openobserve/">
<File Path="infrastructure/helm/chart-openobserve/.helmignore" />
Expand All @@ -58,7 +67,6 @@
<File Path="infrastructure/helm/chart-openobserve/templates/hpa.yaml" />
<File Path="infrastructure/helm/chart-openobserve/templates/httproute.yaml" />
<File Path="infrastructure/helm/chart-openobserve/templates/ingress.yaml" />
<File Path="infrastructure/helm/chart-openobserve/templates/nodeport.yaml" />
<File Path="infrastructure/helm/chart-openobserve/templates/pvc.yaml" />
<File Path="infrastructure/helm/chart-openobserve/templates/service.yaml" />
<File Path="infrastructure/helm/chart-openobserve/templates/serviceaccount.yaml" />
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ gRPC endpoint is exposed on NodePort `31889` (Helm) or host port `31889` (Docker

Traces, metrics and logs from the sample app shown live in the Aspire Dashboard UI:

![](./assets/Aspire-Dashboard.webp)
![Aspire Dashboard demo](https://raw.githubusercontent.com/thorstenalpers/OpenTelemetryExtension.Configuration/main/assets/Aspire-Dashboard.webp)

### Jaeger — `appsettings.jaeger.json`

Expand All @@ -440,7 +440,7 @@ Traces, metrics and logs from the sample app shown live in the Aspire Dashboard

Traces from the sample app shown in the Jaeger UI:

![](./assets/Jaeger.webp)
![Jaeger demo](https://raw.githubusercontent.com/thorstenalpers/OpenTelemetryExtension.Configuration/main/assets/Jaeger.webp)

### OpenObserve — HTTP/protobuf — `appsettings.openobserve-http.json`

Expand All @@ -456,8 +456,7 @@ Traces from the sample app shown in the Jaeger UI:

The same telemetry explored in the OpenObserve UI:

![](./assets/OpenObserve.webp)

![OpenObserve demo](https://raw.githubusercontent.com/thorstenalpers/OpenTelemetryExtension.Configuration/main/assets/OpenObserve.webp)

---

Expand Down
Empty file.
6 changes: 3 additions & 3 deletions infrastructure/helm/helm-install-aspire-dashboard.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ helm repo add aspire-dashboard https://kube-the-home.github.io/aspire-dashboard-
helm repo update

helm uninstall aspire-dashboard
helm install aspire-dashboard aspire-dashboard/aspire-dashboard --version 1.28.3 -f values.aspire-dashboard.yaml
helm install aspire-dashboard aspire-dashboard/aspire-dashboard --version 1.28.3 -f aspire-dashboard.values.yaml

kubectl delete -f nodeports.aspire-dashboard.yaml
kubectl apply -f nodeports.aspire-dashboard.yaml
kubectl delete -f aspire-dashboard.nodeports.yaml
kubectl apply -f aspire-dashboard.nodeports.yaml

REM helm show values aspire-dashboard/aspire-dashboard > values.aspire-dashboard.yaml

Expand Down
17 changes: 17 additions & 0 deletions release-notes/v2.0.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# v2.0.1

_Released: 2026-06-12_

## Changed
- Updated `OpenTelemetry.Exporter.OpenTelemetryProtocol` and
`OpenTelemetry.Extensions.Hosting` to 1.16.0.

## Fixed
- OTLP signal URLs no longer contain a double slash when the endpoint has no
path: `http://localhost:4318` now exports to `http://localhost:4318/v1/traces`
instead of `http://localhost:4318//v1/traces`.
- The `service.version` resource attribute now reports the host application's
version (entry assembly) instead of the library's own version.
- `AddTelemetry(Action<TelemetryOptions>)` now throws the documented
`ArgumentNullException` instead of a `NullReferenceException` when the
configure delegate is `null`.
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,3 @@ public IntegrationFactAttribute()
}
}
}

// Like IntegrationFact, but additionally requires a reachable SQL Server.
public sealed class SqlIntegrationFactAttribute : FactAttribute
{
public SqlIntegrationFactAttribute()
{
if (!Reachability.OpenObserveAvailable)
{
Skip = "OpenObserve is not reachable on localhost:30117 — start it via infrastructure/helm/helm-install-openobserve.cmd.";
}
else if (!Reachability.SqlServerAvailable)
{
Skip = "SQL Server is not reachable on localhost:31433 — start it via infrastructure/helm/helm-install-sqlserver.cmd.";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace OpenTelemetryExtension.Configuration.IntegrationTests.Utils;

// Like IntegrationFact, but additionally requires a reachable SQL Server.
public sealed class SqlIntegrationFactAttribute : FactAttribute
{
public SqlIntegrationFactAttribute()
{
if (!Reachability.OpenObserveAvailable)
{
Skip = "OpenObserve is not reachable on localhost:30117 — start it via infrastructure/helm/helm-install-openobserve.cmd.";
}
else if (!Reachability.SqlServerAvailable)
{
Skip = "SQL Server is not reachable on localhost:31433 — start it via infrastructure/helm/helm-install-sqlserver.cmd.";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.7.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="10.7.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="6.1.3" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.16.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.16.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
Expand Down
Loading
Loading