dotnet-structured-logging
Log pipeline design and operations for .NET distributed systems. Covers log aggregation architecture (ELK, Seq, Grafana
Loki), structured query patterns for each platform, log sampling and volume management strategies, PII scrubbing and
destructuring policies, and cross-service correlation beyond single-service log scopes. This skill addresses what
happens after log emission -- the pipeline, query, and operations layer.
Scope
- Log aggregation architecture (ELK, Seq, Grafana Loki)
- Structured query patterns per platform
- Log sampling and volume management strategies
- PII scrubbing and destructuring policies
- Cross-service correlation and distributed context
Out of scope
- Log emission mechanics (Serilog/NLog/MEL, LoggerMessage, sinks, OTel export) -- see [skill:dotnet-observability]
- Application configuration and options pattern -- see [skill:dotnet-csharp-configuration]
- Distributed tracing setup and trace context propagation -- see [skill:dotnet-observability]
Cross-references: [skill:dotnet-observability] for log emission, Serilog/MEL configuration, and OpenTelemetry logging
export, [skill:dotnet-csharp-configuration] for appsettings.json configuration patterns used in log pipeline setup.
Log Aggregation Architecture
Architecture Options
| Platform |
Ingest |
Storage |
Query |
Best for |
| ELK (Elasticsearch, Logstash, Kibana) |
Logstash / Filebeat |
Elasticsearch |
KQL in Kibana |
Large-scale, flexible schema, full-text search |
| Seq |
HTTP API / Serilog sink |
Built-in |
Seq signal expressions |
.NET-native, developer-friendly, structured queries |
| Grafana Loki |
Promtail / OTel Collector |
Loki (label-indexed) |
LogQL |
Cost-effective, Grafana ecosystem, label-based queries |
| Azure Monitor |
OTel Collector / Application Insights SDK |
Log Analytics workspace |
KQL (Kusto) |
Azure-native, integrated alerting, cost management |
Recommended Pipeline Patterns
Pattern 1: OTel Collector as central router
App (OTLP) --> OTel Collector --> Elasticsearch / Loki / Azure Monitor
|
+--> Sampling / filtering / PII scrub
```text
The OpenTelemetry Collector acts as a vendor-neutral log router. Applications emit logs via OTLP; the collector handles filtering, sampling, enrichment, and routing to one or more backends. This decouples applications from backend choice.
```yaml
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
processors:
batch:
timeout: 5s
send_batch_size: 1024
filter:
logs:
exclude:
match_type: strict
bodies:
- "Health check endpoint hit"
exporters:
elasticsearch:
endpoints: ["https://es-cluster:9200"]
logs_index: "app-logs"
loki:
endpoint: "http://loki:3100/loki/api/v1/push"
service:
pipelines:
logs:
receivers: [otlp]
processors: [batch, filter]
exporters: [elasticsearch, loki]
```text
**Pattern 2: Direct sink (smaller deployments)**
```text
App (Serilog) --> Seq / Elasticsearch sink
```text
For smaller systems or development environments, Serilog sinks write directly to the aggregation platform. This avoids the OTel Collector but couples the application to the backend.
### .NET Application OTLP Configuration
For .NET application-side OTLP log export configuration (`builder.Logging.AddOpenTelemetry()`), see [skill:dotnet-observability]. The OTLP endpoint is configured via environment variables (`OTEL_EXPORTER_OTLP_ENDPOINT`), keeping application code backend-agnostic.
---
## Structured Query Patterns
Structured logs store each property as a queryable field. The query syntax differs by platform but the concepts are consistent: filter by property name, value, severity, and time range.
### Kibana KQL (Elasticsearch / ELK)
```text
# Find errors for a specific order
level: "Error" AND OrderId: "abc-123"
# Find slow operations (custom Duration property)
Duration > 5000 AND ServiceName: "order-api"
# Wildcard on message template
message: "Failed to process*" AND NOT level: "Debug"
# Time-scoped with correlation
TraceId: "0af7651916cd43dd8448eb211c80319c" AND @timestamp >= "2025-01-15T10:00:00"
```text
### Seq Signal Expressions
```text
# Find errors for a specific order
@Level = 'Error' and OrderId = 'abc-123'
# Find slow operations
Duration > 5000 and Application = 'order-api'
# Free-text search combined with structured filter
@Message like '%timeout%' and @Level in ['Warning', 'Error']
# Correlation across services
TraceId = '0af7651916cd43dd8448eb211c80319c'
```text
Seq signals are saved queries that trigger alerts. Define signals for recurring patterns (e.g., "Payment failures > 10/min") and attach notification channels.
### Grafana LogQL (Loki)
```text
# Filter by labels then regex on log line
{service_name="order-api"} |= "Error" | json | OrderId="abc-123"
# Structured field extraction and filtering
{service_name="order-api"} | json | Duration > 5000
# Count errors per service over time (for dashboards)
sum(rate({service_name=~".+"} |= "Error" [5m])) by (service_name)
```json
### Azure Monitor KQL (Kusto)
```kusto
// Find errors for a specific order
traces
| where severityLevel >= 3
| where customDimensions.OrderId == "abc-123"
| order by timestamp desc
// Slow operations
traces
| where toint(customDimensions.Duration) > 5000
| where cloud_RoleName == "order-api"
// Cross-service correlation
union traces, exceptions
| where operation_Id == "0af7651916cd43dd8448eb211c80319c"
| order by timestamp asc
```text
---
## Log Sampling and Volume Management
High-throughput systems can generate millions of log events per minute. Without sampling, storage costs and query performance degrade rapidly.
### Sampling Strategies
| Strategy | How it works | Use when |
|----------|-------------|----------|
| **Head-based** | Decide to sample before processing | Consistent per-request; simple to implement |
| **Tail-based** | Decide to sample after processing | Keep all errors/slow requests, drop routine logs |
| **Level-based** | Sample by severity | Always keep Warning+, sample Debug/Info |
| **Dynamic** | Adjust rate based on volume | Handle traffic spikes without config changes |
### OTel Collector Log Filtering
The `filter` processor in the OTel Collector drops log records at the pipeline level before they reach exporters. Use it to exclude noisy low-severity logs and reduce storage volume.
Note: The `tail_sampling` processor operates on **traces** (spans), not logs. For log volume management, use the `filter` and `transform` processors instead.
```yaml
processors:
filter:
logs:
exclude:
match_type: regexp
# Drop Debug and Trace logs at the collector level
severity_texts: ["DEBUG", "TRACE"]
exclude:
match_type: strict
# Exclude health check noise
bodies:
- "Health check endpoint hit"
transform:
log_statements:
- context: log
conditions:
# Keep all Warning+ logs unconditionally
- severity_number >= SEVERITY_NUMBER_WARN
statements: []
```text
### Application-Level Sampling with Serilog
```csharp
// Serilog.Expressions package for conditional log filtering
builder.Host.UseSerilog((context, loggerConfiguration) =>
{
loggerConfiguration
.ReadFrom.Configuration(context.Configuration)
// Drop health check logs entirely
.Filter.ByExcluding("RequestPath = '/health/ready'")
// Sample Debug logs at 10%
.Filter.ByExcluding(
"@Level = 'Debug' and Hash(@i) % 10 != 0");
});
```text
**Key packages:**
```xml
<PackageReference Include="Serilog.Expressions" Version="5.*" />
```xml
### Volume Management Checklist
1. **Set retention policies** per index/stream (e.g., 30 days for Info, 90 days for Error)
2. **Use log level filtering** to suppress noisy framework categories at the source
3. **Exclude health check endpoints** from request logging
4. **Apply index lifecycle management** (ILM in Elasticsearch, retention policies in Loki)
5. **Monitor ingestion rates** and set budget alerts on storage costs
---
## PII Scrubbing and Destructuring Policies
Logs must not contain personally identifiable information (PII) in production. GDPR, HIPAA, and SOC 2 require that sensitive data is masked or excluded from log storage.
### Property-Level Masking with Enrichers
```csharp
// Enricher that masks known-sensitive properties on every log event
public sealed class PiiMaskingEnricher : ILogEventEnricher
{
private static readonly HashSet<string> s_sensitiveKeys = new(
StringComparer.OrdinalIgnoreCase)
{
"Email", "PhoneNumber", "IpAddress",
"CreditCard", "SSN", "Password"
};
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)
{
var propertiesToMask = logEvent.Properties
.Where(p => s_sensitiveKeys.Contains(p.Key))
.Select(p => p.Key)
.ToList();
foreach (var key in propertiesToMask)
{
logEvent.AddOrUpdateProperty(
factory.CreateProperty(key, "***REDACTED***"));
}
}
}
// Registration
loggerConfiguration.Enrich.With<PiiMaskingEnricher>();
```text
### OTel Collector Attribute Processing
```yaml
processors:
attributes:
actions:
# Mask email addresses using regex
- key: user.email
action: update
value: "***@redacted.com"
# Remove sensitive attributes entirely
- key: http.request.header.authorization
action: delete
- key: user.password
action: delete
```text
### PII Scrubbing Checklist
1. **Identify PII fields** -- email, phone, IP, SSN, credit card, auth tokens, cookies
2. **Apply at the earliest point** -- enricher or OTel processor, not at query time
3. **Audit log templates** -- ensure structured log templates do not capture PII as named properties
4. **Test with compliance team** -- validate scrubbing rules against regulatory requirements
5. **Use separate retention** for audit logs that legitimately require PII (with encryption at rest)
---
## Cross-Service Correlation
In distributed systems, a single user request may traverse multiple services. Correlation enables tracing a request across all services and reconstructing the full event timeline.
### W3C Trace Context Correlation
The primary correlation mechanism is the W3C `traceparent` header, which propagates automatically through `HttpClient` when OpenTelemetry instrumentation is configured (see [skill:dotnet-observability]). All log events emitted within a traced request include `TraceId` and `SpanId` properties.
```csharp
// Query all logs for a distributed operation across services
// In Seq:
TraceId = '0af7651916cd43dd8448eb211c80319c'
// In Kibana:
TraceId: "0af7651916cd43dd8448eb211c80319c"
// In Azure Monitor:
traces | where operation_Id == "0af7651916cd43dd8448eb211c80319c"
```text
### Custom Correlation IDs
When trace context is insufficient (e.g., async workflows spanning message queues, batch jobs, or external system callbacks), add custom correlation IDs:
```csharp
// Propagate a business correlation ID through Serilog LogContext
public sealed class CorrelationIdMiddleware(RequestDelegate next)
{
private const string CorrelationHeader = "X-Correlation-Id";
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers[CorrelationHeader]
.FirstOrDefault() ?? Guid.NewGuid().ToString("N");
context.Response.Headers[CorrelationHeader] = correlationId;
using (LogContext.PushProperty("CorrelationId", correlationId))
{
await next(context);
}
}
}
// Registration
app.UseMiddleware<CorrelationIdMiddleware>();
```text
### Message Queue Correlation
For asynchronous messaging (Azure Service Bus, RabbitMQ), propagate correlation through message properties:
```csharp
// Producer -- attach correlation to message
var message = new ServiceBusMessage(payload)
{
CorrelationId = Activity.Current?.TraceId.ToString()
?? Guid.NewGuid().ToString("N"),
ApplicationProperties =
{
["BusinessCorrelationId"] = orderId.ToString()
}
};
// Consumer -- restore correlation in log scope
processor.ProcessMessageAsync += async args =>
{
using var scope = logger.BeginScope(new Dictionary<string, object>
{
["CorrelationId"] = args.Message.CorrelationId,
["BusinessCorrelationId"] =
args.Message.ApplicationProperties["BusinessCorrelationId"]
});
logger.LogInformation("Processing message {MessageId}", args.Message.MessageId);
await ProcessAsync(args.Message, args.CancellationToken);
};
```text
### Correlation Best Practices
| Practice | Rationale |
|----------|-----------|
| Always include `TraceId` in log output | Enables log-to-trace joins in observability platforms |
| Use `CorrelationId` for business flows | Survives async gaps where trace context resets |
| Store correlation IDs in message headers | Enables end-to-end tracing through queues |
| Include correlation in error responses | Enables support teams to look up the full trace |
| Use Serilog `LogContext.PushProperty` or MEL `BeginScope` | Automatically attaches to all log events in scope |
---
## Agent Gotchas
1. **Do not conflate log emission with log pipeline** -- this skill covers pipeline, query, and operations. For Serilog/MEL configuration, enrichers, sink registration, and source-generated LoggerMessage, see [skill:dotnet-observability].
2. **Do not store PII in production logs** -- apply masking enrichers or OTel processor rules at the pipeline level. Redacting after storage is insufficient for compliance.
3. **Do not skip log sampling for high-throughput services** -- unsampled Debug/Info logs in a service handling thousands of requests per second will overwhelm storage and degrade query performance. Use tail-based sampling to keep all errors and slow requests.
4. **Do not hardcode aggregation platform endpoints in application code** -- use environment variables (`OTEL_EXPORTER_OTLP_ENDPOINT`) or configuration so the same image works across environments.
5. **Do not rely solely on TraceId for business correlation** -- trace context resets at async boundaries (message queues, scheduled jobs). Add explicit business correlation IDs for workflows that span these boundaries.
6. **Do not forget retention policies** -- logs without retention policies accumulate indefinitely, increasing costs and slowing queries. Set per-severity retention (e.g., 30 days for Info, 90 days for Error).
---
## References
- [OpenTelemetry Collector configuration](https://opentelemetry.io/docs/collector/configuration/)
- [Seq documentation](https://docs.datalust.co/docs)
- [Seq signal expressions](https://docs.datalust.co/docs/the-seq-query-language)
- [Grafana Loki LogQL](https://grafana.com/docs/loki/latest/query/)
- [Elasticsearch KQL syntax](https://www.elastic.co/guide/en/kibana/current/kuery-query.html)
- [Serilog.Expressions](https://github.com/serilog/serilog-expressions)
- [Serilog PII masking](https://github.com/serilog/serilog/wiki/Structured-Data#masking-sensitive-data)
- [Azure Monitor KQL reference](https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/)
- [W3C Trace Context specification](https://www.w3.org/TR/trace-context/)
Code Navigation (Serena MCP)
Primary approach: Use Serena symbol operations for efficient code navigation:
- Find definitions:
serena_find_symbol instead of text search
- Understand structure:
serena_get_symbols_overview for file organization
- Track references:
serena_find_referencing_symbols for impact analysis
- Precise edits:
serena_replace_symbol_body for clean modifications
When to use Serena vs traditional tools:
- Use Serena: Navigation, refactoring, dependency analysis, precise edits
- Use Read/Grep: Reading full files, pattern matching, simple text operations
- Fallback: If Serena unavailable, traditional tools work fine
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"
1---2name: dotnet-structured-logging-83description: Designs log pipelines. Aggregation, structured queries, sampling, PII scrubbing, correlation.4license: MIT5---67# dotnet-structured-logging89Log pipeline design and operations for .NET distributed systems. Covers log aggregation architecture (ELK, Seq, Grafana10Loki), structured query patterns for each platform, log sampling and volume management strategies, PII scrubbing and11destructuring policies, and cross-service correlation beyond single-service log scopes. This skill addresses what12happens _after_ log emission -- the pipeline, query, and operations layer.1314## Scope1516- Log aggregation architecture (ELK, Seq, Grafana Loki)17- Structured query patterns per platform18- Log sampling and volume management strategies19- PII scrubbing and destructuring policies20- Cross-service correlation and distributed context2122## Out of scope2324- Log emission mechanics (Serilog/NLog/MEL, LoggerMessage, sinks, OTel export) -- see [skill:dotnet-observability]25- Application configuration and options pattern -- see [skill:dotnet-csharp-configuration]26- Distributed tracing setup and trace context propagation -- see [skill:dotnet-observability]2728Cross-references: [skill:dotnet-observability] for log emission, Serilog/MEL configuration, and OpenTelemetry logging29export, [skill:dotnet-csharp-configuration] for appsettings.json configuration patterns used in log pipeline setup.3031---3233## Log Aggregation Architecture3435### Architecture Options3637| Platform | Ingest | Storage | Query | Best for |38| ----------------------------------------- | ----------------------------------------- | ----------------------- | ---------------------- | ------------------------------------------------------ |39| **ELK** (Elasticsearch, Logstash, Kibana) | Logstash / Filebeat | Elasticsearch | KQL in Kibana | Large-scale, flexible schema, full-text search |40| **Seq** | HTTP API / Serilog sink | Built-in | Seq signal expressions | .NET-native, developer-friendly, structured queries |41| **Grafana Loki** | Promtail / OTel Collector | Loki (label-indexed) | LogQL | Cost-effective, Grafana ecosystem, label-based queries |42| **Azure Monitor** | OTel Collector / Application Insights SDK | Log Analytics workspace | KQL (Kusto) | Azure-native, integrated alerting, cost management |4344### Recommended Pipeline Patterns4546#### Pattern 1: OTel Collector as central router4748````text4950App (OTLP) --> OTel Collector --> Elasticsearch / Loki / Azure Monitor51 |52 +--> Sampling / filtering / PII scrub5354```text5556The OpenTelemetry Collector acts as a vendor-neutral log router. Applications emit logs via OTLP; the collector handles filtering, sampling, enrichment, and routing to one or more backends. This decouples applications from backend choice.5758```yaml5960# otel-collector-config.yaml61receivers:62 otlp:63 protocols:64 grpc:65 endpoint: "0.0.0.0:4317"66 http:67 endpoint: "0.0.0.0:4318"6869processors:70 batch:71 timeout: 5s72 send_batch_size: 102473 filter:74 logs:75 exclude:76 match_type: strict77 bodies:78 - "Health check endpoint hit"7980exporters:81 elasticsearch:82 endpoints: ["https://es-cluster:9200"]83 logs_index: "app-logs"84 loki:85 endpoint: "http://loki:3100/loki/api/v1/push"8687service:88 pipelines:89 logs:90 receivers: [otlp]91 processors: [batch, filter]92 exporters: [elasticsearch, loki]9394```text9596**Pattern 2: Direct sink (smaller deployments)**9798```text99100App (Serilog) --> Seq / Elasticsearch sink101102```text103104For smaller systems or development environments, Serilog sinks write directly to the aggregation platform. This avoids the OTel Collector but couples the application to the backend.105106### .NET Application OTLP Configuration107108For .NET application-side OTLP log export configuration (`builder.Logging.AddOpenTelemetry()`), see [skill:dotnet-observability]. The OTLP endpoint is configured via environment variables (`OTEL_EXPORTER_OTLP_ENDPOINT`), keeping application code backend-agnostic.109110---111112## Structured Query Patterns113114Structured logs store each property as a queryable field. The query syntax differs by platform but the concepts are consistent: filter by property name, value, severity, and time range.115116### Kibana KQL (Elasticsearch / ELK)117118```text119120# Find errors for a specific order121level: "Error" AND OrderId: "abc-123"122123# Find slow operations (custom Duration property)124Duration > 5000 AND ServiceName: "order-api"125126# Wildcard on message template127message: "Failed to process*" AND NOT level: "Debug"128129# Time-scoped with correlation130TraceId: "0af7651916cd43dd8448eb211c80319c" AND @timestamp >= "2025-01-15T10:00:00"131132```text133134### Seq Signal Expressions135136```text137138# Find errors for a specific order139@Level = 'Error' and OrderId = 'abc-123'140141# Find slow operations142Duration > 5000 and Application = 'order-api'143144# Free-text search combined with structured filter145@Message like '%timeout%' and @Level in ['Warning', 'Error']146147# Correlation across services148TraceId = '0af7651916cd43dd8448eb211c80319c'149150```text151152Seq signals are saved queries that trigger alerts. Define signals for recurring patterns (e.g., "Payment failures > 10/min") and attach notification channels.153154### Grafana LogQL (Loki)155156```text157158# Filter by labels then regex on log line159{service_name="order-api"} |= "Error" | json | OrderId="abc-123"160161# Structured field extraction and filtering162{service_name="order-api"} | json | Duration > 5000163164# Count errors per service over time (for dashboards)165sum(rate({service_name=~".+"} |= "Error" [5m])) by (service_name)166167```json168169### Azure Monitor KQL (Kusto)170171```kusto172173// Find errors for a specific order174traces175| where severityLevel >= 3176| where customDimensions.OrderId == "abc-123"177| order by timestamp desc178179// Slow operations180traces181| where toint(customDimensions.Duration) > 5000182| where cloud_RoleName == "order-api"183184// Cross-service correlation185union traces, exceptions186| where operation_Id == "0af7651916cd43dd8448eb211c80319c"187| order by timestamp asc188189```text190191---192193## Log Sampling and Volume Management194195High-throughput systems can generate millions of log events per minute. Without sampling, storage costs and query performance degrade rapidly.196197### Sampling Strategies198199| Strategy | How it works | Use when |200|----------|-------------|----------|201| **Head-based** | Decide to sample before processing | Consistent per-request; simple to implement |202| **Tail-based** | Decide to sample after processing | Keep all errors/slow requests, drop routine logs |203| **Level-based** | Sample by severity | Always keep Warning+, sample Debug/Info |204| **Dynamic** | Adjust rate based on volume | Handle traffic spikes without config changes |205206### OTel Collector Log Filtering207208The `filter` processor in the OTel Collector drops log records at the pipeline level before they reach exporters. Use it to exclude noisy low-severity logs and reduce storage volume.209210Note: The `tail_sampling` processor operates on **traces** (spans), not logs. For log volume management, use the `filter` and `transform` processors instead.211212```yaml213214processors:215 filter:216 logs:217 exclude:218 match_type: regexp219 # Drop Debug and Trace logs at the collector level220 severity_texts: ["DEBUG", "TRACE"]221 exclude:222 match_type: strict223 # Exclude health check noise224 bodies:225 - "Health check endpoint hit"226 transform:227 log_statements:228 - context: log229 conditions:230 # Keep all Warning+ logs unconditionally231 - severity_number >= SEVERITY_NUMBER_WARN232 statements: []233234```text235236### Application-Level Sampling with Serilog237238```csharp239240// Serilog.Expressions package for conditional log filtering241builder.Host.UseSerilog((context, loggerConfiguration) =>242{243 loggerConfiguration244 .ReadFrom.Configuration(context.Configuration)245 // Drop health check logs entirely246 .Filter.ByExcluding("RequestPath = '/health/ready'")247 // Sample Debug logs at 10%248 .Filter.ByExcluding(249 "@Level = 'Debug' and Hash(@i) % 10 != 0");250});251252```text253254**Key packages:**255256```xml257258<PackageReference Include="Serilog.Expressions" Version="5.*" />259260```xml261262### Volume Management Checklist2632641. **Set retention policies** per index/stream (e.g., 30 days for Info, 90 days for Error)2652. **Use log level filtering** to suppress noisy framework categories at the source2663. **Exclude health check endpoints** from request logging2674. **Apply index lifecycle management** (ILM in Elasticsearch, retention policies in Loki)2685. **Monitor ingestion rates** and set budget alerts on storage costs269270---271272## PII Scrubbing and Destructuring Policies273274Logs must not contain personally identifiable information (PII) in production. GDPR, HIPAA, and SOC 2 require that sensitive data is masked or excluded from log storage.275276### Property-Level Masking with Enrichers277278```csharp279280// Enricher that masks known-sensitive properties on every log event281public sealed class PiiMaskingEnricher : ILogEventEnricher282{283 private static readonly HashSet<string> s_sensitiveKeys = new(284 StringComparer.OrdinalIgnoreCase)285 {286 "Email", "PhoneNumber", "IpAddress",287 "CreditCard", "SSN", "Password"288 };289290 public void Enrich(LogEvent logEvent, ILogEventPropertyFactory factory)291 {292 var propertiesToMask = logEvent.Properties293 .Where(p => s_sensitiveKeys.Contains(p.Key))294 .Select(p => p.Key)295 .ToList();296297 foreach (var key in propertiesToMask)298 {299 logEvent.AddOrUpdateProperty(300 factory.CreateProperty(key, "***REDACTED***"));301 }302 }303}304305// Registration306loggerConfiguration.Enrich.With<PiiMaskingEnricher>();307308```text309310### OTel Collector Attribute Processing311312```yaml313314processors:315 attributes:316 actions:317 # Mask email addresses using regex318 - key: user.email319 action: update320 value: "***@redacted.com"321 # Remove sensitive attributes entirely322 - key: http.request.header.authorization323 action: delete324 - key: user.password325 action: delete326327```text328329### PII Scrubbing Checklist3303311. **Identify PII fields** -- email, phone, IP, SSN, credit card, auth tokens, cookies3322. **Apply at the earliest point** -- enricher or OTel processor, not at query time3333. **Audit log templates** -- ensure structured log templates do not capture PII as named properties3344. **Test with compliance team** -- validate scrubbing rules against regulatory requirements3355. **Use separate retention** for audit logs that legitimately require PII (with encryption at rest)336337---338339## Cross-Service Correlation340341In distributed systems, a single user request may traverse multiple services. Correlation enables tracing a request across all services and reconstructing the full event timeline.342343### W3C Trace Context Correlation344345The primary correlation mechanism is the W3C `traceparent` header, which propagates automatically through `HttpClient` when OpenTelemetry instrumentation is configured (see [skill:dotnet-observability]). All log events emitted within a traced request include `TraceId` and `SpanId` properties.346347```csharp348349// Query all logs for a distributed operation across services350// In Seq:351TraceId = '0af7651916cd43dd8448eb211c80319c'352353// In Kibana:354TraceId: "0af7651916cd43dd8448eb211c80319c"355356// In Azure Monitor:357traces | where operation_Id == "0af7651916cd43dd8448eb211c80319c"358359```text360361### Custom Correlation IDs362363When trace context is insufficient (e.g., async workflows spanning message queues, batch jobs, or external system callbacks), add custom correlation IDs:364365```csharp366367// Propagate a business correlation ID through Serilog LogContext368public sealed class CorrelationIdMiddleware(RequestDelegate next)369{370 private const string CorrelationHeader = "X-Correlation-Id";371372 public async Task InvokeAsync(HttpContext context)373 {374 var correlationId = context.Request.Headers[CorrelationHeader]375 .FirstOrDefault() ?? Guid.NewGuid().ToString("N");376377 context.Response.Headers[CorrelationHeader] = correlationId;378379 using (LogContext.PushProperty("CorrelationId", correlationId))380 {381 await next(context);382 }383 }384}385386// Registration387app.UseMiddleware<CorrelationIdMiddleware>();388389```text390391### Message Queue Correlation392393For asynchronous messaging (Azure Service Bus, RabbitMQ), propagate correlation through message properties:394395```csharp396397// Producer -- attach correlation to message398var message = new ServiceBusMessage(payload)399{400 CorrelationId = Activity.Current?.TraceId.ToString()401 ?? Guid.NewGuid().ToString("N"),402 ApplicationProperties =403 {404 ["BusinessCorrelationId"] = orderId.ToString()405 }406};407408// Consumer -- restore correlation in log scope409processor.ProcessMessageAsync += async args =>410{411 using var scope = logger.BeginScope(new Dictionary<string, object>412 {413 ["CorrelationId"] = args.Message.CorrelationId,414 ["BusinessCorrelationId"] =415 args.Message.ApplicationProperties["BusinessCorrelationId"]416 });417418 logger.LogInformation("Processing message {MessageId}", args.Message.MessageId);419 await ProcessAsync(args.Message, args.CancellationToken);420};421422```text423424### Correlation Best Practices425426| Practice | Rationale |427|----------|-----------|428| Always include `TraceId` in log output | Enables log-to-trace joins in observability platforms |429| Use `CorrelationId` for business flows | Survives async gaps where trace context resets |430| Store correlation IDs in message headers | Enables end-to-end tracing through queues |431| Include correlation in error responses | Enables support teams to look up the full trace |432| Use Serilog `LogContext.PushProperty` or MEL `BeginScope` | Automatically attaches to all log events in scope |433434---435436## Agent Gotchas4374381. **Do not conflate log emission with log pipeline** -- this skill covers pipeline, query, and operations. For Serilog/MEL configuration, enrichers, sink registration, and source-generated LoggerMessage, see [skill:dotnet-observability].4392. **Do not store PII in production logs** -- apply masking enrichers or OTel processor rules at the pipeline level. Redacting after storage is insufficient for compliance.4403. **Do not skip log sampling for high-throughput services** -- unsampled Debug/Info logs in a service handling thousands of requests per second will overwhelm storage and degrade query performance. Use tail-based sampling to keep all errors and slow requests.4414. **Do not hardcode aggregation platform endpoints in application code** -- use environment variables (`OTEL_EXPORTER_OTLP_ENDPOINT`) or configuration so the same image works across environments.4425. **Do not rely solely on TraceId for business correlation** -- trace context resets at async boundaries (message queues, scheduled jobs). Add explicit business correlation IDs for workflows that span these boundaries.4436. **Do not forget retention policies** -- logs without retention policies accumulate indefinitely, increasing costs and slowing queries. Set per-severity retention (e.g., 30 days for Info, 90 days for Error).444445---446447## References448449- [OpenTelemetry Collector configuration](https://opentelemetry.io/docs/collector/configuration/)450- [Seq documentation](https://docs.datalust.co/docs)451- [Seq signal expressions](https://docs.datalust.co/docs/the-seq-query-language)452- [Grafana Loki LogQL](https://grafana.com/docs/loki/latest/query/)453- [Elasticsearch KQL syntax](https://www.elastic.co/guide/en/kibana/current/kuery-query.html)454- [Serilog.Expressions](https://github.com/serilog/serilog-expressions)455- [Serilog PII masking](https://github.com/serilog/serilog/wiki/Structured-Data#masking-sensitive-data)456- [Azure Monitor KQL reference](https://learn.microsoft.com/en-us/azure/data-explorer/kusto/query/)457- [W3C Trace Context specification](https://www.w3.org/TR/trace-context/)458````459460## Code Navigation (Serena MCP)461462**Primary approach:** Use Serena symbol operations for efficient code navigation:4634641. **Find definitions**: `serena_find_symbol` instead of text search4652. **Understand structure**: `serena_get_symbols_overview` for file organization4663. **Track references**: `serena_find_referencing_symbols` for impact analysis4674. **Precise edits**: `serena_replace_symbol_body` for clean modifications468469**When to use Serena vs traditional tools:**470471- **Use Serena**: Navigation, refactoring, dependency analysis, precise edits472- **Use Read/Grep**: Reading full files, pattern matching, simple text operations473- **Fallback**: If Serena unavailable, traditional tools work fine474475**Example workflow:**476477```text478# Instead of:479Read: src/Services/OrderService.cs480Grep: "public void ProcessOrder"481482# Use:483serena_find_symbol: "OrderService/ProcessOrder"484serena_get_symbols_overview: "src/Services/OrderService.cs"485```