Application Logging (Console / OpenTelemetry / Serilog / Seq)¶
Note
This page describes the application/host logging layer only - i.e. where the
standard Microsoft.Extensions.Logging events produced by ASP.NET Core itself and by the
WebGIS internals end up. It is independent of the GeoService performance/exception
logging switch (Api:logging-type) described in Configuration of the API application, which writes
dedicated webgis_performance/webgis_exceptions records rather than generic
application log events - the two layers can end up pointed at the same database server, just
at different tables.
The Api, Cms and Portal hosts all share the same setup, so everything below applies
to all three (adjust the file names accordingly, e.g. Cms/appsettings.json).
Important
MicrosoftGeoServiceRequestLogger (the microsoft IGeoServiceRequestLogger, i.e. the
per-request “audit” log, as opposed to the performance timings) logs at LogLevel.Trace -
the most verbose level, disabled by default. Logging:LogLevel:Default is normally
Information, so nothing is written for this category unless it is explicitly overridden
(raising only Default is not enough, since Trace is more verbose than
Information):
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Api.Core.AppCode.Services.Logging.MicrosoftGeoServiceRequestLogger": "Trace"
}
}
}
This can go into appsettings.json, _config/logging.json (see below), or as the
environment variable
Logging__LogLevel__Api.Core.AppCode.Services.Logging.MicrosoftGeoServiceRequestLogger=Trace.
If a specific provider (e.g. Console) has its own narrower Logging:<Provider>:LogLevel
section, that provider needs the same override too.
Under the hood, WebGIS always routes logging through Serilog, which normally only obeys its
own Serilog:MinimumLevel section and ignores Logging:LogLevel entirely; WebGIS bridges
Logging:LogLevel into Serilog’s MinimumLevel automatically, so this standard ASP.NET
Core section keeps working as documented. An explicit Serilog:MinimumLevel/Override
entry for the same category always takes precedence over the bridged Logging:LogLevel
value.
Configuring via _config (recommended for production/Kubernetes)¶
Most customers only touch the _config directory next to the application binaries (it also
holds api.config/cms.config/portal.config) - in a Kubernetes deployment this is
typically the one directory mounted from a ConfigMap/Secret volume, while appsettings.json
and the pod’s environment variables are not (easily) editable per instance. Everything on this
page can therefore also be configured by dropping one or both of these optional files into
_config, with no rebuild and no pod-spec changes required:
``_config/logging.json`` - a regular JSON file merged into the standard configuration, i.e. anything that could otherwise only be configured via
appsettings.jsonor environment variables (Logging,Serilog,OTEL_*keys, …) can be placed here instead. All JSON examples on this page work unchanged as the content of_config/logging.json.``_config/logging.env`` - a simple
KEY=VALUEper line file (like a Docker--env-file,#starts a comment line), loaded as real process environment variables before the app starts. Useful for the standard OpenTelemetryOTEL_*variables shown below, since they are normally set as environment variables rather than nested JSON. An environment variable that is already set on the process/pod always wins over the file.
Example _config/logging.env:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
Both files are optional and take effect for Api, Cms and Portal alike; a missing
file is a no-op. _config/logging.json is loaded after appsettings.json/
appsettings.<Environment>.json, so it overrides them (matching how _config/*.config
already overrides other defaults today).
Console (default)¶
Out of the box, logs go to the console (stdout), formatted as plain text - this is the ASP.NET
Core default and requires no configuration. Log levels are controlled the usual way via the
Logging:LogLevel section in appsettings.json or environment variables
(Logging__LogLevel__Default=Information, …).
For structured/JSON console output (useful when a container log collector like Filebeat/Fluent Bit/Promtail parses stdout), switch the console formatter:
{
"Logging": {
"Console": {
"FormatterName": "json"
}
}
}
OpenTelemetry (OTLP) - recommended for most backends¶
The Api, Cms and Portal hosts export logs, metrics and traces via the OpenTelemetry
Protocol (OTLP) whenever an OTLP endpoint is configured. This is controlled purely by the
standard OpenTelemetry environment variables - no other configuration or rebuild is needed:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc # or "http/protobuf" for port 4318
OTEL_SERVICE_NAME=webgis-api # optional, defaults to the host name
If OTEL_EXPORTER_OTLP_ENDPOINT is not set, no OTLP exporter is created and there is no
overhead.
These can be set as regular process/pod environment variables, or - if only the _config
directory is available - via _config/logging.env (see above).
This is the lowest-effort way to plug WebGIS into most modern observability backends, because they all speak OTLP natively (no WebGIS-specific sink needed): an OpenTelemetry Collector (which can then fan out anywhere), Grafana stack (Loki/Tempo/Mimir), Elastic (8.x+ / Elastic Cloud), Seq, Jaeger, Zipkin, Honeycomb, Datadog, New Relic, Azure Monitor, AWS X-Ray/ADOT, etc.
Note
Previously this was only wired up for local development (.NET Aspire). It is now available in
release builds of Api, Cms and Portal as well.
Request log entries on the trace¶
GeoService requests (GetMap, GetSelection, …, print jobs) each run inside their own
geoservice:{command} {service} trace span (WebGIS.GeoServices ActivitySource, tagged
with webgis.category/command/server/service/map/success). While that span
is active, every IGeoServiceRequestLogger.LogString(...) call (the per-request “audit” trail
- e.g. the underlying HTTP request/response to the real GIS server) is also attached to it as an
OpenTelemetry span event (webgis.geoservice.request, with webgis.server/service/
command tags). In a trace viewer (e.g. the Aspire dashboard’s trace detail view, or the “View
Logs” action there for the correlated log entries), this means the request-audit trail for a
call shows up directly next to/on its span, not just as a separately-filtered log level.
Recording the span event does not depend on the microsoft IGeoServiceRequestLogger’s own
LogLevel.Trace setting (see above) - it is skipped only when nothing is sampling/listening to
the trace.
The request and response are kept as two separate tags instead of being concatenated into one string:
webgis.requestResult- the response (or, for call sites that only log a single value, that value) - typically JSON, and still recognizable as such by tooling since nothing is prefixed in front of it.webgis.requestBody- only present when a request body was actually logged separately (e.g. the outgoing request sent to the upstream GIS server).
The same split applies to the structured log entry itself: with a request body, the request
logger logs requestBody and message as distinct named placeholders (rather than one
pre-joined string), so a JSON response is still parsed as JSON by log backends that recognize
structured fields.
Serilog sinks (SQL Server / PostgreSQL / Seq)¶
For deployments that need logs written directly into a relational database - without standing
up an OpenTelemetry Collector - Api, Cms and Portal additionally run
Serilog alongside the default logging pipeline, so it augments rather
than replaces OTLP/console logging. Serilog is purely config-driven: with no Serilog section
present, nothing changes from the default behavior (console output only).
To add a sink, add a Serilog section to appsettings.json (or
appsettings.Production.json, or environment variables) - or, equivalently, to
_config/logging.json (see above).
SQL Server¶
{
"ConnectionStrings": {
"LogsDb": "Server=sql-host;Database=WebGisLogs;User Id=webgis;Password=***;TrustServerCertificate=True"
},
"Serilog": {
"WriteTo": [
{
"Name": "MSSqlServer",
"Args": {
"connectionString": "LogsDb",
"sinkOptions": {
"tableName": "Logs",
"autoCreateSqlTable": true
}
}
}
]
}
}
PostgreSQL¶
The PostgreSQL sink needs its configuration companion package to be loaded explicitly via
Using, and - unlike MSSqlServer, which ships sensible default columns - it has no
built-in defaults: a Columns section describing which columns to write and how must always
be supplied. Omitting it fails fast at startup with:
System.InvalidOperationException: 'Columns' section not found in provided configuration path:
The Columns section must be a root-level key of the configuration - a sibling of
Serilog/ConnectionStrings, not nested inside Serilog (it is easy to assume
otherwise, since everything else Serilog-related lives under the Serilog section). If it
needs to be placed elsewhere (e.g. to avoid a name clash), point at it explicitly via
Serilog:WriteTo:Args:configurationPath.
{
"ConnectionStrings": {
"LogsDb": "Host=pg-host;Port=5432;Database=webgis_logs;Username=webgis;Password=***"
},
"Serilog": {
"Using": [ "Serilog.Sinks.PostgreSQL.Configuration" ],
"WriteTo": [
{
"Name": "PostgreSQL",
"Args": {
"connectionString": "LogsDb",
"tableName": "logs",
"needAutoCreateTable": true
}
}
]
},
"Columns": {
"message": "RenderedMessageColumnWriter",
"message_template": "MessageTemplateColumnWriter",
"level": {
"Name": "LevelColumnWriter",
"Args": { "renderAsText": true, "dbType": "Varchar" }
},
"raise_date": "TimestampColumnWriter",
"exception": "ExceptionColumnWriter",
"properties": "LogEventSerializedColumnWriter"
}
}
Both sinks are batching sinks (periodic bulk insert), so they have negligible impact on request latency.
Seq¶
Unlike the DB sinks above, Seq is not a relational database - it is a small, self-hostable log server with its own storage engine and UI (see Setting up Seq below for how to run it). The sink just needs a URL and (optionally) an API key - no table/columns to define:
{
"Serilog": {
"WriteTo": [
{
"Name": "Seq",
"Args": {
"serverUrl": "http://seq-host:5341",
"apiKey": "***"
}
}
]
}
}
This, too, is a batching sink with negligible latency impact, and can run alongside the SQL Server/PostgreSQL sink (e.g. DB for long-term retention, Seq for day-to-day search) - Serilog happily writes to any number of configured sinks at once.
Oracle¶
There is currently no well-maintained Serilog sink for Oracle, so a direct DB sink is not offered for Oracle. If the log backend is Oracle-only, use the OTLP path instead (e.g. an OpenTelemetry Collector with an Oracle/JDBC exporter, or export to an OTLP-native backend and query it there).
Viewing & analyzing logs¶
Which tool makes sense depends on which sink(s) are enabled:
Only the SQL Server/PostgreSQL sink, ad-hoc troubleshooting: a regular DB client is enough - no extra tool needed. Azure Data Studio/SSMS (SQL Server) or pgAdmin/DBeaver (PostgreSQL). The structured
Properties/propertiescolumn is JSON (NVARCHAR/jsonb), queryable directly, e.g.:-- SQL Server SELECT TOP 100 * FROM Logs WHERE Level = 'Warning' AND JSON_VALUE(Properties, '$.RequestPath') LIKE '%GetMap%' ORDER BY TimeStamp DESC; -- PostgreSQL SELECT * FROM logs WHERE level = 'Warning' AND properties->>'RequestPath' LIKE '%GetMap%' ORDER BY raise_date DESC LIMIT 100;
This does not give full-text search, dashboards, alerting, or trace correlation though - it is fine for “quick lookup”, not for ongoing analysis.
Dashboards/alerting/search - recommended: Grafana (OSS, self-hostable, free). It can be pointed at what is already configured, with no need to add another sink:
Directly at the SQL Server/PostgreSQL sink table via Grafana’s built-in SQL data sources - dashboards/alerts on top of data already being written today.
At the OTLP path via Loki (logs) + Tempo (traces) + Prometheus/Mimir (metrics) - the de-facto open source stack for OpenTelemetry, and a natural fit since
Api/Cms/Portalalready export OTLP onceOTEL_EXPORTER_OTLP_ENDPOINTis set (see above). This also enables trace/log/metric correlation (e.g. drilling from a slowGetMaptrace into its log lines), which the DB sinks alone cannot provide.
Simplest “just show me the logs” option, no Grafana setup: Seq - single Docker container, understands Serilog’s structured events natively, free for a single user/small team, and accepts OTLP directly too (see below for step-by-step setup).
Customer already runs a cloud/enterprise observability platform: point
OTEL_EXPORTER_OTLP_ENDPOINTat it - Azure Monitor/Application Insights, AWS CloudWatch, Elastic/OpenSearch + Kibana, Datadog, New Relic, … all accept OTLP natively (directly or via an OpenTelemetry Collector), no code changes required.
Setting up Seq¶
Unlike Grafana, Seq has no “SQL data source” concept - it can only show events that were sent to
it directly, either via OTLP or as a Serilog sink. It cannot simply be pointed at an
already-populated Logs/logs table in SQL Server/PostgreSQL and browse that in place; if
that is the goal (view/analyze what the DB sink already collects, without touching anything
else), use Grafana’s built-in SQL data source against that table instead (see above) - no
dual-write, no extra sink required.
If Seq’s own UI/search is still preferred, WebGIS needs to additionally send events to Seq - either as the Seq sink documented above (dual-write, alongside the existing SQL Server/PostgreSQL sink if any), or via OTLP as described below - both are already fully wired up.
No WebGIS-specific sink/package is needed for the OTLP route below - Seq natively implements OTLP ingestion, so that path is purely a matter of pointing the existing OTLP exporter at it.
Run Seq (persists its data in
<local path>, replace<password>with the initial admin password):docker run --name seq -d --restart unless-stopped \ -e ACCEPT_EULA=Y \ -e SEQ_FIRSTRUN_ADMINPASSWORD=<password> \ -v <local path>:/data \ -p 5341:80 \ datalust/seq
In the Seq UI (
http://<seq-host>:5341), create a dedicated API key per app under Settings > API Keys (recommended, not required) - this makes it easy to tellApi/Cms/Portaltraffic apart later in Data > Ingestion.Point WebGIS’s OTLP exporter at Seq’s OTLP endpoint - either as real environment variables, or via
_config/logging.env(see above):OTEL_EXPORTER_OTLP_ENDPOINT=http://<seq-host>:5341/ingest/otlp OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_EXPORTER_OTLP_HEADERS=X-Seq-ApiKey=<api-key-from-step-2>
Note the endpoint is Seq’s ingestion path (
/ingest/otlp), not just the host root - the exporter appends/v1/logs//v1/tracesitself.OTEL_EXPORTER_OTLP_HEADERScan be omitted if no API key was created in step 2.Alternatively - no traces, but structured events with full property fidelity and less config - add the Seq sink to
_config/logging.jsoninstead, using the same URL/API key.Restart
Api/Cms/Portal(environment variables are only read at process startup, so_config/logging.envchanges need a restart, unlike_config/logging.jsonwhich is picked up automatically). Logs and traces (including the GeoService request/performance spans described above) start showing up in the Seq UI immediately - with full trace/span correlation via the Trace menu on each event.
The SQL Server/PostgreSQL sink can keep running in parallel if it is already configured (e.g. for long-term retention/compliance) - Seq is simply an additional, independent consumer of the same telemetry.
Notes¶
Everything on this page (Serilog sinks, OTLP, Seq, Grafana, …) concerns where application log events go; it is independent of the
Api:logging-typeGeoService performance/exception logging switch described in Configuration of the API application, which - since it now also supportssqlserver/postgres/sqlite/oracle- can end up pointed at the same database server, just a different, purpose-builtwebgis_performance/webgis_exceptionstable rather than the genericLogstable the Serilog DB sinks write to.Multiple sinks can be active at once (e.g. console + OTLP + SQL Server) - enable only what is needed.