Observability
This page is Endatix API OpenTelemetry (OTEL_* on the API host). Hub uses the same OTLP names plus Azure Monitor — Hub observability.
Endatix emits logs, metrics and traces through one OpenTelemetry SDK over OTLP. There is no separate logging stack and no vendor SDK in the box.
Telemetry is off by default. With no OTLP endpoint, nothing is exported and no exporter is allocated. One environment variable turns all three signals on.
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
Logs also always go to stdout, whether or not telemetry is configured — that is what docker logs and kubectl logs read.
What you get
| Signal | Source | Notable series / spans |
|---|---|---|
| Metrics | ASP.NET Core, HttpClient, .NET runtime | http.server.request.duration, dotnet.gc.collections, thread pool |
| Traces | Inbound HTTP, outbound HttpClient (including webhook delivery) | one span per request; /health, /alive, /ready excluded |
| Logs | Every ILogger record | carries the TraceId of the request it happened in |
All three share one resource, so a log record and its span agree on service.name.
:::note Runtime metric names
On .NET 9 and later the runtime instrumentation emits dotnet.* metrics (dotnet.gc.collections, dotnet.thread_pool.thread.count). The older process.runtime.dotnet.* names are not emitted. If a dashboard shows nothing, check which names it queries.
:::
Environment variables
Standard OTEL_* variables are authoritative. Anything under Endatix:Telemetry is a fallback, never an override.
Collector endpoint. Setting this enables telemetry.
grpc or http/protobuf.
Per-signal override. Any one of traces, metrics, or logs endpoint also enables telemetry. Signal-specific variables take precedence over the global one, per the OpenTelemetry specification.
Per-signal metrics endpoint.
Per-signal logs endpoint.
service.name on everything exported.
service.version.
Sampling strategy, e.g. parentbased_traceidratio. Pair with OTEL_TRACES_SAMPLER_ARG (0.1 for 10%).
:::warning A malformed endpoint fails startup An unparseable endpoint or an unknown protocol throws at startup, naming the offending value. The alternative is a host that starts and silently exports nothing. :::
Configuration fallback
Every variable above has an appsettings.json equivalent. Environment variables win where both are present.
{
"Endatix": {
"Telemetry": {
"Otlp": { "Endpoint": "http://collector:4317", "Protocol": "grpc" },
"ServiceName": "endatix-api",
"ResourceAttributes": { "deployment.environment": "staging" }
}
}
}
Log levels
Endatix uses the standard Logging section. Levels default to Warning, so exporting without raising the Endatix level ships almost nothing.
The recommended production shape lifts Endatix records for OTLP only, leaving the console terse:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning",
"Endatix": "Warning"
},
"OpenTelemetry": { "LogLevel": { "Endatix": "Information" } },
"Console": { "FormatterName": "json" }
}
}
Logging:OpenTelemetry:LogLevel:* is provider-scoped: it changes what the OTLP exporter receives without touching the console.
Every provider.
Stdout only.
OTLP export only.
The log file only (see below).
File logging
.NET ships no file logging provider, so Endatix includes an optional one. It is disabled by default.
{
"Endatix": {
"Logging": {
"File": {
"Enabled": false,
"Path": "logs/endatix-.log",
"Formatter": "Json",
"RollingInterval": "Day",
"FileSizeLimitBytes": 10485760,
"RollOnFileSizeLimit": true,
"RetainedFileCountLimit": 7
}
}
}
}
Off by design — see the container note below.
Relative paths resolve against the content root, not the working directory. The rotation suffix is inserted before the extension.
Json or Text. JSON keeps structured properties queryable.
Infinite, Year, Month, Day, Hour, Minute.
With this off the sink stops writing at the limit rather than rolling.
Older files are deleted as new ones roll.
Enabling file logging never silences the console. Both receive every record.
:::danger Enabling this in a container needs a writable volume
The Helm chart runs with readOnlyRootFilesystem: true and only /tmp writable. Enabling file logging without mounting a writable volume at the configured path will fail startup with a message naming the directory. Use the chart's extraVolumes / extraVolumeMounts passthrough.
:::
Self-hosted collector
A minimal OpenTelemetry Collector that accepts OTLP and forwards metrics to Prometheus, logs to Loki and traces to Tempo:
receivers:
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
batch: {}
exporters:
prometheus:
endpoint: 0.0.0.0:8889
otlphttp/loki:
endpoint: http://loki:3100/otlp
otlp/tempo:
endpoint: tempo:4317
tls: { insecure: true }
service:
pipelines:
metrics: { receivers: [otlp], processors: [batch], exporters: [prometheus] }
logs: { receivers: [otlp], processors: [batch], exporters: [otlphttp/loki] }
traces: { receivers: [otlp], processors: [batch], exporters: [otlp/tempo] }
Point Endatix at it with OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317.
Local development
docker compose up starts an Aspire Dashboard alongside the API and Hub, with both already exporting to it. Image: mcr.microsoft.com/dotnet/aspire-dashboard:13.4.2. No extra configuration.
- Dashboard UI — http://localhost:18888
- OTLP/gRPC ingest —
localhost:18889
Both ports bind to 127.0.0.1 deliberately: the dashboard is unauthenticated and renders every request, log line and form payload the platform handles. Never pair it with a 0.0.0.0 publish, and never set DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS in a deployed manifest.
To export to it from an app running outside Docker:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:18889 \
OTEL_EXPORTER_OTLP_PROTOCOL=grpc \
OTEL_SERVICE_NAME=endatix-api \
dotnet run
Hub outside Docker uses the same endpoint with OTEL_SERVICE_NAME=endatix-hub — Hub observability.
Migrating from the Serilog section
Endatix no longer reads a Serilog configuration section. A host that still has one starts normally and logs a warning naming the section — the section is ignored, which means the levels in it no longer apply.
Levels
| Before | After |
|---|---|
Serilog:MinimumLevel:Default | Logging:LogLevel:Default |
Serilog:MinimumLevel:Override:Microsoft | Logging:LogLevel:Microsoft |
Serilog:MinimumLevel:Override:Endatix | Logging:LogLevel:Endatix |
Serilog level names map to .NET names: Verbose → Trace, Debug → Debug, Information → Information, Warning → Warning, Error → Error, Fatal → Critical.
Sinks
| Before | After |
|---|---|
Serilog:WriteTo → Console | Always on. Shape with Logging:Console:FormatterName (json or simple) |
Serilog:WriteTo → File | Endatix:Logging:File:Enabled: true — one key to flip, no package to install |
Serilog:WriteTo → ApplicationInsights | See Bring your own telemetry |
Serilog:WriteTo:File:Args maps key-for-key onto Endatix:Logging:File: path → Path, rollingInterval → RollingInterval, fileSizeLimitBytes → FileSizeLimitBytes, rollOnFileSizeLimit → RollOnFileSizeLimit, retainedFileCountLimit → RetainedFileCountLimit.
:::caution Check your file path while migrating
If your old configuration used an absolute path such as /logs/log-.txt, confirm the process can actually write there. Relative paths now resolve against the content root; absolute paths are left exactly as given.
:::
Code
| Before | After |
|---|---|
endatix.Logging.ConfigureSerilog(cfg => …) | endatix.Logging.Configure(logging => …) |
endatix.Logging.ConfigureBootstrapLogger(…) | Removed — startup logging is configured from Logging |
The replacement takes an ILoggingBuilder, so it works with any provider rather than only Serilog:
builder.Host.ConfigureEndatixWithDefaults(endatix =>
{
endatix.Logging.Configure(logging => logging.AddAzureWebAppDiagnostics());
});
:::warning builder.Logging.AddX() in Program.cs does not survive
Endatix registers logging inside IHostBuilder.ConfigureServices, which runs at Build() and calls ClearProviders() first — so anything added directly to builder.Logging beforehand is discarded. endatix.Logging.Configure(...) exists because it runs after that point.
:::