Synergy.Platform.Observability
1.0.0-preview.1
dotnet add package Synergy.Platform.Observability --version 1.0.0-preview.1
NuGet\Install-Package Synergy.Platform.Observability -Version 1.0.0-preview.1
<PackageReference Include="Synergy.Platform.Observability" Version="1.0.0-preview.1" />
<PackageVersion Include="Synergy.Platform.Observability" Version="1.0.0-preview.1" />
<PackageReference Include="Synergy.Platform.Observability" />
paket add Synergy.Platform.Observability --version 1.0.0-preview.1
#r "nuget: Synergy.Platform.Observability, 1.0.0-preview.1"
#:package Synergy.Platform.Observability@1.0.0-preview.1
#addin nuget:?package=Synergy.Platform.Observability&version=1.0.0-preview.1&prerelease
#tool nuget:?package=Synergy.Platform.Observability&version=1.0.0-preview.1&prerelease
Synergy.Platform.Observability
One dependency puts a Synergy service on Grafana Cloud: OpenTelemetry traces and metrics,
Serilog JSON logs carrying TraceId/SpanId, and user/tenant context on both — so a single
log line links through to the full distributed trace behind it.
Two packages, one concern:
| Package | For | |
|---|---|---|
src/ |
Synergy.Platform.Observability (NuGet) | .NET APIs |
react/ |
@synergytek/observability (npm, git install) | React micro-frontends |
.NET
dotnet add package Synergy.Platform.Observability --prerelease
using Synergy.Platform.Observability;
builder.AddSynergyObservability(o =>
{
o.Marten = true; // event-store spans
o.Wolverine = true; // keeps traces alive across Kafka
o.MassTransit = true;
});
var app = builder.Build();
app.UseAuthentication();
app.UseSynergyObservability(); // after auth - the claims must be populated
That replaces roughly 30 lines and four package references per service.
What it does
- Serilog → JSON on stdout, enriched with
TraceId,SpanId,service,version - Traces → ASP.NET Core, HttpClient, EF Core, plus whichever sources you opt into
- Metrics → ASP.NET Core, HttpClient, .NET runtime
- Export → OTLP, configured entirely from the environment
- Middleware →
user_idandtenant_idonto every log line and span
Configuration is environment-only, by design
There is no endpoint, protocol or header setting on the options object. That is not an oversight.
The previous per-service code set Endpoint and Protocol in C#. Two consequences:
OTEL_EXPORTER_OTLP_PROTOCOL was ignored, and setting Endpoint programmatically
suppresses the /v1/traces path the HTTP exporter appends — so requests went to the root
and vanished. Both failed silently. Every service was fully instrumented and exporting
nothing, for months, with no error in any log.
A package that cannot accept an endpoint cannot have that bug reintroduced.
| Variable | Required | Notes |
|---|---|---|
OTEL_SERVICE_NAME |
recommended | Falls back to the assembly name. Also used as Serilog's service field, so both join on it |
OTEL_EXPORTER_OTLP_ENDPOINT |
to export | Unset ⇒ the exporter is not registered (see below) |
OTEL_EXPORTER_OTLP_PROTOCOL |
for Grafana Cloud | Must be http/protobuf. Unset, the SDK defaults to gRPC and silently sends nothing to an HTTP gateway |
OTEL_EXPORTER_OTLP_HEADERS |
for Grafana Cloud | Authorization=Basic%20<base64> — from a Secret, never a values file |
OTEL_TRACES_SAMPLER / _ARG |
recommended | parentbased_traceidratio / 0.1 |
Unset endpoint means no exporter at all. The SDK would otherwise default to
localhost:4317 and retry forever against nothing — noise in local development, and
misleading in a half-configured environment where it looks like it is trying. Logging still
works; only export is skipped.
Sampling and Marten
o.Marten = true is worth having — it is how a slow request shows you the slow query. But
Marten's TrackConnections = TrackLevel.Normal emits a span per database connection.
At 100% sampling that alone will exhaust a free-tier allowance in days. Keep
OTEL_TRACES_SAMPLER_ARG at 0.1 and raise it only while chasing something specific.
Target frameworks
net8.0 and net10.0. Most Synergy APIs are on 8; Synergy.Base.Workflow is already on
10. A single-target package would force one of them to move before it could adopt this.
React
// package.json
"@synergytek/observability": "git+https://github.com/SynergyTek/Synergy.Platform.Observability.git#main"
Same git-install mechanism as @synergytek/react. Plain ESM, no JSX, no build step.
// src/observability.js
import { initSynergyObservability } from '@synergytek/observability';
initSynergyObservability({
url: '__FARO_URL__',
name: '__SERVICE_NAME__',
version: '__SERVICE_VERSION__',
environment: '__FARO_ENVIRONMENT__',
tracePropagationTargets: ['https://api.aitalkx.com'],
});
// src/index.jsx
import { FaroErrorBoundary } from '@synergytek/observability';
<FaroErrorBoundary fallback={<ErrorFallback />}>
<App />
</FaroErrorBoundary>
Why the placeholders are passed through, not read from the environment
A browser bundle has no environment. These apps are built once and configured per
environment by entrypoint.sh, which seds __PLACEHOLDER__ tokens inside the built
JavaScript at container start. That is what makes a config change an apply-and-restart
rather than a rebuild — verified: the tokens survive rsbuild minification intact.
The package keeps the url.startsWith('__') guard, so an app deployed without FARO_URL
stays silent instead of throwing on every page load.
Add any new variable to the for var in ... list in the app's entrypoint.sh, or the
substitution will not happen.
tracePropagationTargets is not optional in practice
Without it the browser will not attach trace headers to cross-origin API calls, and frontend sessions never join up with backend traces — which is most of the value. List your API origins.
Where to install it
In the host shell, not each remote. Synergy.BackOffice.Host.UI loads all six
micro-frontends via module federation, so one instance there covers every one of them. A
federated remote's own entry point may never run when the host consumes it, and several
Faro instances would fight over session IDs.
Pass router: false in a remote that does not own a router.
What this replaces
Before, in every API:
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.Enrich.WithSpan()
.Enrich.WithProperty("service", Environment.GetEnvironmentVariable("OTEL_SERVICE_NAME") ?? "unknown")
.Enrich.WithProperty("version", Assembly.GetExecutingAssembly().GetName().Version?.ToString())
.WriteTo.Console(new CompactJsonFormatter()));
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService(/* ... */))
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("Marten")
.AddSource("Wolverine")
.AddSource("MassTransit")
.AddOtlpExporter())
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("MassTransit")
.AddOtlpExporter());
app.Use(async (ctx, next) => { /* ~15 lines of user/tenant plumbing */ });
After: the six lines at the top of this page, and four fewer PackageReference entries.
Multiply by seven APIs and seven UIs. More to the point: when the next thing is wrong, it is wrong in one repo.
Releasing
Versioning follows Synergy.Platform.Marten: <Version> in the csproj, consumers pin
exact.
dotnet pack src/Synergy.Platform.Observability/Synergy.Platform.Observability.csproj -c Release -o artifacts
dotnet nuget push artifacts/Synergy.Platform.Observability.1.0.0-preview.1.nupkg \
--source https://api.nuget.org/v3/index.json --api-key $NUGET_API_KEY
The npm side has no registry step — consumers install from the git URL, so a merge to main
is the release. Tag it so apps can pin: #v1.0.0-preview.1 instead of #main.
Related
Synergy-Devops/OBSERVABILITY.md— deployment runbook: Grafana Cloud setup, environment variables, rollout order, and troubleshooting for every failure hit during the kptcl-dev rolloutSynergy.Base.TemplatePR #82 — the original hand-rolled implementation this package generalises
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- OpenTelemetry.Exporter.OpenTelemetryProtocol (>= 1.17.0)
- OpenTelemetry.Extensions.Hosting (>= 1.17.0)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.17.0)
- OpenTelemetry.Instrumentation.EntityFrameworkCore (>= 1.17.0-beta.1)
- OpenTelemetry.Instrumentation.Http (>= 1.17.0)
- OpenTelemetry.Instrumentation.Runtime (>= 1.17.0)
- Serilog.AspNetCore (>= 8.0.0)
- Serilog.Enrichers.Span (>= 3.1.0)
- Serilog.Formatting.Compact (>= 3.0.0)
-
net8.0
- OpenTelemetry.Exporter.OpenTelemetryProtocol (>= 1.17.0)
- OpenTelemetry.Extensions.Hosting (>= 1.17.0)
- OpenTelemetry.Instrumentation.AspNetCore (>= 1.17.0)
- OpenTelemetry.Instrumentation.EntityFrameworkCore (>= 1.17.0-beta.1)
- OpenTelemetry.Instrumentation.Http (>= 1.17.0)
- OpenTelemetry.Instrumentation.Runtime (>= 1.17.0)
- Serilog.AspNetCore (>= 8.0.0)
- Serilog.Enrichers.Span (>= 3.1.0)
- Serilog.Formatting.Compact (>= 3.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.1 | 496 | 8/11/2026 |