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
133 changes: 133 additions & 0 deletions .github/workflows/build-samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ on:
- "dotnet-8-essentials/background-jobs-hostedservice-queues/**"
- "dotnet-8-essentials/configuration-secrets-environments/**"
- "dotnet-8-essentials/core-features-get-started/**"
- "dotnet-8-essentials/observability-opentelemetry/**"
- ".github/workflows/build-samples.yml"

pull_request:
Expand All @@ -52,6 +53,7 @@ on:
- "dotnet-8-essentials/background-jobs-hostedservice-queues/**"
- "dotnet-8-essentials/configuration-secrets-environments/**"
- "dotnet-8-essentials/core-features-get-started/**"
- "dotnet-8-essentials/observability-opentelemetry/**"
- ".github/workflows/build-samples.yml"

workflow_dispatch:
Expand Down Expand Up @@ -1428,3 +1430,134 @@ jobs:
)"

test "${invalid_status}" = "400"
test-opentelemetry-signals:
name: Test correlated OpenTelemetry signals sample
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Check out repository
uses: actions/checkout@v5

- name: Install .NET 10 SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"

- name: Restore
run: >
dotnet restore
dotnet-8-essentials/observability-opentelemetry/OpenTelemetrySignalsMinimal.slnx

- name: Build
run: >
dotnet build
dotnet-8-essentials/observability-opentelemetry/OpenTelemetrySignalsMinimal.slnx
--configuration Release
--no-restore

- name: Test
run: >
dotnet test
dotnet-8-essentials/observability-opentelemetry/OpenTelemetrySignalsMinimal.slnx
--configuration Release
--no-build

- name: Verify OpenTelemetry package baseline
shell: bash
run: |
project="dotnet-8-essentials/observability-opentelemetry/src/OpenTelemetrySignalsMinimal/OpenTelemetrySignalsMinimal.csproj"

test "$(grep --count '<PackageReference' "${project}")" = "3"

grep --fixed-strings 'Include="OpenTelemetry.Extensions.Hosting"' "${project}"
grep --fixed-strings 'Include="OpenTelemetry.Exporter.Console"' "${project}"
grep --fixed-strings 'Include="OpenTelemetry.Instrumentation.AspNetCore"' "${project}"

test "$(grep --count 'Version="1.17.0"' "${project}")" = "3"

- name: Smoke-test correlated signals API
shell: bash
run: |
app_log="${RUNNER_TEMP}/otel-signals.log"

dotnet run \
--project dotnet-8-essentials/observability-opentelemetry/src/OpenTelemetrySignalsMinimal/OpenTelemetrySignalsMinimal.csproj \
--configuration Release \
--no-build \
--urls http://127.0.0.1:5099 \
>"${app_log}" 2>&1 &

app_pid="$!"

cleanup() {
kill "${app_pid}" 2>/dev/null || true
wait "${app_pid}" 2>/dev/null || true
}

trap cleanup EXIT

for attempt in $(seq 1 40); do
if curl --fail --silent \
http://127.0.0.1:5099/ \
>"${RUNNER_TEMP}/otel-root.json"; then
break
fi

if ! kill -0 "${app_pid}" 2>/dev/null; then
cat "${app_log}"
exit 1
fi

sleep 0.25
done

root="$(cat "${RUNNER_TEMP}/otel-root.json")"

expected_root='{"sample":"correlated-opentelemetry-signals","exporter":"console","activitySource":"DotNetGuide.Observability.Checkout","meter":"DotNetGuide.Observability.Checkout"}'

test "${root}" = "${expected_root}"

checkout="$(
curl --fail --silent \
--request POST \
--header 'Content-Type: application/json' \
--data '{"channel":"WEB","itemCount":2}' \
http://127.0.0.1:5099/checkout
)"

printf '%s\n' "${checkout}" |
grep --extended-regexp --quiet \
'^\{"status":"accepted","channel":"web","itemCount":2,"traceId":"[0-9a-fA-F]{32}"\}$'

invalid_channel="$(
curl --silent \
--output "${RUNNER_TEMP}/invalid-channel.json" \
--write-out '%{http_code}' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"channel":"customer-92823","itemCount":2}' \
http://127.0.0.1:5099/checkout
)"

test "${invalid_channel}" = "400"

invalid_count="$(
curl --silent \
--output "${RUNNER_TEMP}/invalid-count.json" \
--write-out '%{http_code}' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"channel":"mobile","itemCount":11}' \
http://127.0.0.1:5099/checkout
)"

test "${invalid_count}" = "400"

sleep 1.5

grep --fixed-strings --quiet 'Checkout.Process' "${app_log}"
grep --fixed-strings --quiet 'checkout.requests' "${app_log}"
grep --fixed-strings --quiet 'Checkout accepted' "${app_log}"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
| [`dotnet-8-essentials/background-jobs-hostedservice-queues`](dotnet-8-essentials/background-jobs-hostedservice-queues/) | Focused ASP.NET Core Minimal API demonstrating a bounded Channel queue, asynchronous backpressure, a BackgroundService consumer, fresh scoped handlers, safe in-memory job status, exception isolation, cancellation, and integration tests | [.NET 8 Background Jobs: IBackgroundTaskQueue, BackgroundService & Production-Ready Patterns](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/background-jobs-hostedservice-queues/) |
| [`dotnet-8-essentials/configuration-secrets-environments`](dotnet-8-essentials/configuration-secrets-environments/) | Focused .NET 10 Minimal API demonstrating layered configuration precedence, prefixed environment variables, command-line overrides, strongly typed options, startup validation, User Secrets metadata, a lightweight feature flag, and safe Development-only diagnostics | [.NET 8 Configuration & Secrets Management: Typed Options, User Secrets & Feature Flags](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/configuration-secrets-environments/) |
| [`dotnet-8-essentials/core-features-get-started`](dotnet-8-essentials/core-features-get-started/) | Focused .NET 10 Native AOT Minimal API demonstrating CreateSlimBuilder, source-generated JSON, typed DI, AOT-safe endpoints, analyzer-aware publishing, and direct native-binary smoke testing | [.NET 8 Essentials: Core Features & Getting Started](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/core-features-get-started/) |
| [`dotnet-8-essentials/observability-opentelemetry`](dotnet-8-essentials/observability-opentelemetry/) | Focused .NET 10 OpenTelemetry companion demonstrating ASP.NET Core request instrumentation, custom ActivitySource spans, low-cardinality Meter metrics, structured ILogger events, automatic log-to-trace correlation, console export, and deterministic tests without external observability infrastructure | [.NET 8 Observability with OpenTelemetry: Tracing, Metrics & Structured Logging](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/observability-opentelemetry/) |

## Companion articles
- [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/)
Expand Down Expand Up @@ -177,6 +178,7 @@ tutorials/
| |-- ConfigPrecedenceMinimal.Tests.csproj
| `-- ConfigurationTests.cs
| `-- core-features-get-started/
| `-- observability-opentelemetry/
| |-- NativeAotEssentialsMinimal.slnx
| |-- README.md
| |-- src/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Solution>
<Project Path="src/OpenTelemetrySignalsMinimal/OpenTelemetrySignalsMinimal.csproj" />
<Project Path="tests/OpenTelemetrySignalsMinimal.Tests/OpenTelemetrySignalsMinimal.Tests.csproj" />
</Solution>
124 changes: 124 additions & 0 deletions dotnet-8-essentials/observability-opentelemetry/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# OpenTelemetry Correlated Signals &mdash; Minimal .NET 10 Companion

> Full tutorial: [.NET 8 Observability with OpenTelemetry: Tracing, Metrics & Structured Logging](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/observability-opentelemetry/)

## What this sample demonstrates

- ASP.NET Core Minimal API with OpenTelemetry 1.17.0
- Custom `ActivitySource` for manual tracing via `Checkout.Process` spans
- Custom `Meter` with a `counter` (`checkout.requests`) and a `histogram` (`checkout.duration`)
- Low-cardinality metric tags (`checkout.channel`, `checkout.outcome`)
- Source-generated structured `ILogger` events via `LoggerMessage`
- Automatic log-to-trace correlation from the active `Activity`
- Learning-only OpenTelemetry Console Exporter output
- Input validation with rejection telemetry
- `ActivityListener` and `MeterListener` unit tests
- Deterministic API smoke tests without external backends

## Architecture

```
POST /checkout
ASP.NET Core request Activity
custom Checkout.Process Activity
structured ILogger event
checkout.requests counter
checkout.duration histogram
OpenTelemetry SDK
Console Exporter
```

## File structure

```
dotnet-8-essentials/observability-opentelemetry/
├── OpenTelemetrySignalsMinimal.slnx
├── README.md
├── src/OpenTelemetrySignalsMinimal/
│ ├── OpenTelemetrySignalsMinimal.csproj
│ ├── Program.cs
│ ├── Models/
│ │ └── CheckoutModels.cs
│ └── Telemetry/
│ ├── CheckoutLog.cs
│ └── CheckoutTelemetry.cs
└── tests/OpenTelemetrySignalsMinimal.Tests/
├── OpenTelemetrySignalsMinimal.Tests.csproj
└── OpenTelemetrySignalsTests.cs
```

## Prerequisites

- .NET 10.0 SDK or later
- No Docker, collector, or external backend required

## Run

```bash
cd dotnet-8-essentials/observability-opentelemetry
dotnet restore OpenTelemetrySignalsMinimal.slnx
dotnet build --configuration Release
dotnet run --project src/OpenTelemetrySignalsMinimal --configuration Release --urls http://127.0.0.1:5099
```

## Verify

```bash
# Root endpoint — fixed metadata
curl --silent http://127.0.0.1:5099/
# {"sample":"correlated-opentelemetry-signals","exporter":"console","activitySource":"DotNetGuide.Observability.Checkout","meter":"DotNetGuide.Observability.Checkout"}

# Successful checkout — returns trace ID
curl --silent --request POST \
--header "Content-Type: application/json" \
--data '{"channel":"WEB","itemCount":2}' \
http://127.0.0.1:5099/checkout
# {"status":"accepted","channel":"web","itemCount":2,"traceId":"<32 hex chars>"}

# Invalid channel — returns 400
curl --silent --request POST \
--header "Content-Type: application/json" \
--data '{"channel":"customer-92823","itemCount":2}' \
http://127.0.0.1:5099/checkout

# Out-of-range item count — returns 400
curl --silent --request POST \
--header "Content-Type: application/json" \
--data '{"channel":"mobile","itemCount":11}' \
http://127.0.0.1:5099/checkout
```

## Tests

```bash
dotnet test --configuration Release --no-build
```

## Important boundary

- **Console Exporter** is for learning and debugging only. It is **not** recommended for production use, and its text output is not a standardized transport contract.
- OTLP, Collector, Docker Compose, Jaeger, Prometheus, Grafana, and cloud exporters are intentionally excluded.
- EF Core, HttpClient instrumentation, external service calls, queues, sampling, baggage, and health checks are not included.
- The full tutorial at [dotnet-guide.com](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/observability-opentelemetry/) covers production observability architecture with OTLP &rarr; Collector &rarr; backends.

## Verification table

| Item | Value |
|------|-------|
| Target framework | `net10.0` |
| OpenTelemetry.Extensions.Hosting | 1.17.0 |
| OpenTelemetry.Exporter.Console | 1.17.0 |
| OpenTelemetry.Instrumentation.AspNetCore | 1.17.0 |
| External services | None |
| Last reviewed | 2026-08-08 |

## License

This sample is provided for educational purposes as part of the [DOTNET GUIDE](https://www.dotnet-guide.com) tutorial series.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace OpenTelemetrySignalsMinimal.Models;

public sealed record SampleInfo(
string Sample,
string Exporter,
string ActivitySource,
string Meter);

public sealed record CheckoutRequest(
string? Channel,
int ItemCount);

public sealed record CheckoutResponse(
string Status,
string Channel,
int ItemCount,
string TraceId);

public sealed record ApiError(
string Code,
string Message);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.17.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
</ItemGroup>

</Project>
Loading
Loading