OTEL Logging with Grafana Loki and .NET
Logging is an essential part of any application, and Grafana Loki is a solid choice for self hosting your own logging infrastructure. In this post we will setup a new .NET minimal API, and then integrate logging to push to a Loki instance using OTEL (OpenTelemetry).
Creating the .NET API
Let's get started with creating our API, for this we shall use VS Code as the .NET CLI.
dotnet new web -o LoggingApi
cd LoggingApi
code -r ../LoggingApiOnce created let's add the endpoint we will use for our testing. Update the Program.cs file to include the following:
app.MapGet("/log", (ILogger<Program> logger) =>
{
if (logger.IsEnabled(LogLevel.Information))
logger.LogInformation("Logging a message at {time:HH:mm:ss} on {machine}", DateTime.Now, Environment.MachineName);
return Results.Ok();
});Let's test this and and make sure everything is good so far, start the application using dotnet run and then navigate to the /log endpoint in your browser. You should see a log in the terminal similar to the image below.

Integrating OTEL Logging
To integrate OTEL logging add the following code before the var app = builder.Build() call.
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
if (useOtlpExporter)
{
logging.AddOtlpExporter();
}
else
{
Console.WriteLine("OTEL_EXPORTER_OTLP_ENDPOINT is not set. Skipping OTLP exporter configuration.");
}
});This will configure our app for OTEL logging, and if a endpoint is set provision an OTEL exporter.
We will need two new packages for this, so add them as below:
dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocolSetting up local Grafana + Loki
Next we want to setup a development environment for Grafana, Alloy and Loki, to enable us to test our solution. Grafana is our dashboard and log viewing application, Alloy is the OTEL collector and Loki is the log aggregator.
Create the following files in the root of our project:
services:
alloy:
image: grafana/alloy:v1.13.1
ports:
- 12345:12345
- 4317:4317
- 4318:4318
volumes:
- ./config.alloy:/etc/alloy/config.alloy
command: run --server.http.listen-addr=0.0.0.0:12345 --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
depends_on:
- loki
loki:
image: grafana/loki:3.5.10
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml
- loki-data:/loki
command: -config.file=/etc/loki/local-config.yaml
grafana:
image: grafana/grafana:12.3
environment:
- GF_FEATURE_TOGGLES_ENABLE=grafanaManagedRecordingRules
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_BASIC_ENABLED=false
ports:
- 3001:3000/tcp
entrypoint:
- sh
- -euc
- |
mkdir -p /etc/grafana/provisioning/datasources
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
orgId: 1
url: 'http://loki:3100'
basicAuth: false
isDefault: true
version: 1
editable: true
EOF
/run.sh
volumes:
loki-data:compose.yml
otelcol.receiver.otlp "default" {
http {
endpoint = "0.0.0.0:4318"
}
grpc {
endpoint = "0.0.0.0:4317"
}
output {
logs = [otelcol.processor.batch.default.input]
}
}
otelcol.processor.batch "default" {
output {
logs = [otelcol.exporter.otlphttp.loki.input]
}
}
otelcol.exporter.otlphttp "loki" {
client {
endpoint = "http://loki:3100/otlp"
}
}
config.alloy
auth_enabled: false
server:
http_listen_port: 3100
common:
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
replication_factor: 1
path_prefix: /loki
schema_config:
configs:
- from: 2020-05-15
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
filesystem:
directory: /loki/chunks
compactor:
working_directory: /loki/compactor
retention_enabled: true
compaction_interval: 24h
delete_request_store: filesystem
limits_config:
retention_period: 720h # 30 days
loki-config.yaml
Once all our files are created spin up services using docker compose up -d . Once started we should be able to access the Grafana interface on http://localhost:3001.
Testing our Setup
To test our logging setup add the following values to our appsettings.Development.json :
{
// ...
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
"OTEL_SERVICE_NAME": "LoggingApi"
}
Start our program with dotnet run and then navigate to the /log endpoint in your browser (or a tool such as Postman). You should see the login in the terminal console, and, if you navigate to Drilldown > Logs in the Grafana UI you should see the log messages there.

Conclusion
Using the above steps we can setup a simple logging environment for our .NET application, if we wanted too this could be extended to also support metrics and traces in the future.
The source code for this blog can be found at: https://liamsgit.dev/LiamPietralla/dotnet-loki-otel-demo