Skip to content

Commit 4cbb79d

Browse files
Merge pull request #21 from dotnet-guide-com/feature/opentelemetry-signals-sample
Add lightweight OpenTelemetry signals sample
2 parents 183f864 + 1028982 commit 4cbb79d

11 files changed

Lines changed: 865 additions & 0 deletions

File tree

‎.github/workflows/build-samples.yml‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ on:
2626
- "dotnet-8-essentials/background-jobs-hostedservice-queues/**"
2727
- "dotnet-8-essentials/configuration-secrets-environments/**"
2828
- "dotnet-8-essentials/core-features-get-started/**"
29+
- "dotnet-8-essentials/observability-opentelemetry/**"
2930
- ".github/workflows/build-samples.yml"
3031

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

5759
workflow_dispatch:
@@ -1428,3 +1430,134 @@ jobs:
14281430
)"
14291431
14301432
test "${invalid_status}" = "400"
1433+
test-opentelemetry-signals:
1434+
name: Test correlated OpenTelemetry signals sample
1435+
runs-on: ubuntu-latest
1436+
1437+
permissions:
1438+
contents: read
1439+
1440+
steps:
1441+
- name: Check out repository
1442+
uses: actions/checkout@v5
1443+
1444+
- name: Install .NET 10 SDK
1445+
uses: actions/setup-dotnet@v5
1446+
with:
1447+
dotnet-version: "10.0.x"
1448+
1449+
- name: Restore
1450+
run: >
1451+
dotnet restore
1452+
dotnet-8-essentials/observability-opentelemetry/OpenTelemetrySignalsMinimal.slnx
1453+
1454+
- name: Build
1455+
run: >
1456+
dotnet build
1457+
dotnet-8-essentials/observability-opentelemetry/OpenTelemetrySignalsMinimal.slnx
1458+
--configuration Release
1459+
--no-restore
1460+
1461+
- name: Test
1462+
run: >
1463+
dotnet test
1464+
dotnet-8-essentials/observability-opentelemetry/OpenTelemetrySignalsMinimal.slnx
1465+
--configuration Release
1466+
--no-build
1467+
1468+
- name: Verify OpenTelemetry package baseline
1469+
shell: bash
1470+
run: |
1471+
project="dotnet-8-essentials/observability-opentelemetry/src/OpenTelemetrySignalsMinimal/OpenTelemetrySignalsMinimal.csproj"
1472+
1473+
test "$(grep --count '<PackageReference' "${project}")" = "3"
1474+
1475+
grep --fixed-strings 'Include="OpenTelemetry.Extensions.Hosting"' "${project}"
1476+
grep --fixed-strings 'Include="OpenTelemetry.Exporter.Console"' "${project}"
1477+
grep --fixed-strings 'Include="OpenTelemetry.Instrumentation.AspNetCore"' "${project}"
1478+
1479+
test "$(grep --count 'Version="1.17.0"' "${project}")" = "3"
1480+
1481+
- name: Smoke-test correlated signals API
1482+
shell: bash
1483+
run: |
1484+
app_log="${RUNNER_TEMP}/otel-signals.log"
1485+
1486+
dotnet run \
1487+
--project dotnet-8-essentials/observability-opentelemetry/src/OpenTelemetrySignalsMinimal/OpenTelemetrySignalsMinimal.csproj \
1488+
--configuration Release \
1489+
--no-build \
1490+
--urls http://127.0.0.1:5099 \
1491+
>"${app_log}" 2>&1 &
1492+
1493+
app_pid="$!"
1494+
1495+
cleanup() {
1496+
kill "${app_pid}" 2>/dev/null || true
1497+
wait "${app_pid}" 2>/dev/null || true
1498+
}
1499+
1500+
trap cleanup EXIT
1501+
1502+
for attempt in $(seq 1 40); do
1503+
if curl --fail --silent \
1504+
http://127.0.0.1:5099/ \
1505+
>"${RUNNER_TEMP}/otel-root.json"; then
1506+
break
1507+
fi
1508+
1509+
if ! kill -0 "${app_pid}" 2>/dev/null; then
1510+
cat "${app_log}"
1511+
exit 1
1512+
fi
1513+
1514+
sleep 0.25
1515+
done
1516+
1517+
root="$(cat "${RUNNER_TEMP}/otel-root.json")"
1518+
1519+
expected_root='{"sample":"correlated-opentelemetry-signals","exporter":"console","activitySource":"DotNetGuide.Observability.Checkout","meter":"DotNetGuide.Observability.Checkout"}'
1520+
1521+
test "${root}" = "${expected_root}"
1522+
1523+
checkout="$(
1524+
curl --fail --silent \
1525+
--request POST \
1526+
--header 'Content-Type: application/json' \
1527+
--data '{"channel":"WEB","itemCount":2}' \
1528+
http://127.0.0.1:5099/checkout
1529+
)"
1530+
1531+
printf '%s\n' "${checkout}" |
1532+
grep --extended-regexp --quiet \
1533+
'^\{"status":"accepted","channel":"web","itemCount":2,"traceId":"[0-9a-fA-F]{32}"\}$'
1534+
1535+
invalid_channel="$(
1536+
curl --silent \
1537+
--output "${RUNNER_TEMP}/invalid-channel.json" \
1538+
--write-out '%{http_code}' \
1539+
--request POST \
1540+
--header 'Content-Type: application/json' \
1541+
--data '{"channel":"customer-92823","itemCount":2}' \
1542+
http://127.0.0.1:5099/checkout
1543+
)"
1544+
1545+
test "${invalid_channel}" = "400"
1546+
1547+
invalid_count="$(
1548+
curl --silent \
1549+
--output "${RUNNER_TEMP}/invalid-count.json" \
1550+
--write-out '%{http_code}' \
1551+
--request POST \
1552+
--header 'Content-Type: application/json' \
1553+
--data '{"channel":"mobile","itemCount":11}' \
1554+
http://127.0.0.1:5099/checkout
1555+
)"
1556+
1557+
test "${invalid_count}" = "400"
1558+
1559+
sleep 1.5
1560+
1561+
grep --fixed-strings --quiet 'Checkout.Process' "${app_log}"
1562+
grep --fixed-strings --quiet 'checkout.requests' "${app_log}"
1563+
grep --fixed-strings --quiet 'Checkout accepted' "${app_log}"

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Each sample folder contains a focused implementation of one tutorial topic. The
3131
| [`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/) |
3232
| [`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/) |
3333
| [`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/) |
34+
| [`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/) |
3435

3536
## Companion articles
3637
- [Common Microsoft.Extensions.AI mistakes](https://www.dotnet-guide.com/articles/dotnet-ai/microsoft-extensions-ai-common-mistakes/)
@@ -177,6 +178,7 @@ tutorials/
177178
| |-- ConfigPrecedenceMinimal.Tests.csproj
178179
| `-- ConfigurationTests.cs
179180
| `-- core-features-get-started/
181+
| `-- observability-opentelemetry/
180182
| |-- NativeAotEssentialsMinimal.slnx
181183
| |-- README.md
182184
| |-- src/
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<Solution>
2+
<Project Path="src/OpenTelemetrySignalsMinimal/OpenTelemetrySignalsMinimal.csproj" />
3+
<Project Path="tests/OpenTelemetrySignalsMinimal.Tests/OpenTelemetrySignalsMinimal.Tests.csproj" />
4+
</Solution>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# OpenTelemetry Correlated Signals &mdash; Minimal .NET 10 Companion
2+
3+
> Full tutorial: [.NET 8 Observability with OpenTelemetry: Tracing, Metrics & Structured Logging](https://www.dotnet-guide.com/tutorials/dotnet-8-essentials/observability-opentelemetry/)
4+
5+
## What this sample demonstrates
6+
7+
- ASP.NET Core Minimal API with OpenTelemetry 1.17.0
8+
- Custom `ActivitySource` for manual tracing via `Checkout.Process` spans
9+
- Custom `Meter` with a `counter` (`checkout.requests`) and a `histogram` (`checkout.duration`)
10+
- Low-cardinality metric tags (`checkout.channel`, `checkout.outcome`)
11+
- Source-generated structured `ILogger` events via `LoggerMessage`
12+
- Automatic log-to-trace correlation from the active `Activity`
13+
- Learning-only OpenTelemetry Console Exporter output
14+
- Input validation with rejection telemetry
15+
- `ActivityListener` and `MeterListener` unit tests
16+
- Deterministic API smoke tests without external backends
17+
18+
## Architecture
19+
20+
```
21+
POST /checkout
22+
↓
23+
ASP.NET Core request Activity
24+
↓
25+
custom Checkout.Process Activity
26+
↓
27+
structured ILogger event
28+
↓
29+
checkout.requests counter
30+
↓
31+
checkout.duration histogram
32+
↓
33+
OpenTelemetry SDK
34+
↓
35+
Console Exporter
36+
```
37+
38+
## File structure
39+
40+
```
41+
dotnet-8-essentials/observability-opentelemetry/
42+
├── OpenTelemetrySignalsMinimal.slnx
43+
├── README.md
44+
├── src/OpenTelemetrySignalsMinimal/
45+
│ ├── OpenTelemetrySignalsMinimal.csproj
46+
│ ├── Program.cs
47+
│ ├── Models/
48+
│ │ └── CheckoutModels.cs
49+
│ └── Telemetry/
50+
│ ├── CheckoutLog.cs
51+
│ └── CheckoutTelemetry.cs
52+
└── tests/OpenTelemetrySignalsMinimal.Tests/
53+
├── OpenTelemetrySignalsMinimal.Tests.csproj
54+
└── OpenTelemetrySignalsTests.cs
55+
```
56+
57+
## Prerequisites
58+
59+
- .NET 10.0 SDK or later
60+
- No Docker, collector, or external backend required
61+
62+
## Run
63+
64+
```bash
65+
cd dotnet-8-essentials/observability-opentelemetry
66+
dotnet restore OpenTelemetrySignalsMinimal.slnx
67+
dotnet build --configuration Release
68+
dotnet run --project src/OpenTelemetrySignalsMinimal --configuration Release --urls http://127.0.0.1:5099
69+
```
70+
71+
## Verify
72+
73+
```bash
74+
# Root endpoint — fixed metadata
75+
curl --silent http://127.0.0.1:5099/
76+
# {"sample":"correlated-opentelemetry-signals","exporter":"console","activitySource":"DotNetGuide.Observability.Checkout","meter":"DotNetGuide.Observability.Checkout"}
77+
78+
# Successful checkout — returns trace ID
79+
curl --silent --request POST \
80+
--header "Content-Type: application/json" \
81+
--data '{"channel":"WEB","itemCount":2}' \
82+
http://127.0.0.1:5099/checkout
83+
# {"status":"accepted","channel":"web","itemCount":2,"traceId":"<32 hex chars>"}
84+
85+
# Invalid channel — returns 400
86+
curl --silent --request POST \
87+
--header "Content-Type: application/json" \
88+
--data '{"channel":"customer-92823","itemCount":2}' \
89+
http://127.0.0.1:5099/checkout
90+
91+
# Out-of-range item count — returns 400
92+
curl --silent --request POST \
93+
--header "Content-Type: application/json" \
94+
--data '{"channel":"mobile","itemCount":11}' \
95+
http://127.0.0.1:5099/checkout
96+
```
97+
98+
## Tests
99+
100+
```bash
101+
dotnet test --configuration Release --no-build
102+
```
103+
104+
## Important boundary
105+
106+
- **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.
107+
- OTLP, Collector, Docker Compose, Jaeger, Prometheus, Grafana, and cloud exporters are intentionally excluded.
108+
- EF Core, HttpClient instrumentation, external service calls, queues, sampling, baggage, and health checks are not included.
109+
- 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.
110+
111+
## Verification table
112+
113+
| Item | Value |
114+
|------|-------|
115+
| Target framework | `net10.0` |
116+
| OpenTelemetry.Extensions.Hosting | 1.17.0 |
117+
| OpenTelemetry.Exporter.Console | 1.17.0 |
118+
| OpenTelemetry.Instrumentation.AspNetCore | 1.17.0 |
119+
| External services | None |
120+
| Last reviewed | 2026-08-08 |
121+
122+
## License
123+
124+
This sample is provided for educational purposes as part of the [DOTNET GUIDE](https://www.dotnet-guide.com) tutorial series.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace OpenTelemetrySignalsMinimal.Models;
2+
3+
public sealed record SampleInfo(
4+
string Sample,
5+
string Exporter,
6+
string ActivitySource,
7+
string Meter);
8+
9+
public sealed record CheckoutRequest(
10+
string? Channel,
11+
int ItemCount);
12+
13+
public sealed record CheckoutResponse(
14+
string Status,
15+
string Channel,
16+
int ItemCount,
17+
string TraceId);
18+
19+
public sealed record ApiError(
20+
string Code,
21+
string Message);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
12+
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.17.0" />
13+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
14+
</ItemGroup>
15+
16+
</Project>

0 commit comments

Comments
 (0)