Notes / Projects / Platform Shipsolid / 02 Service Onboarding

Logs Instrumentation Guide

How to instrument a service for **logs** on the ShipSolid observability platform.

Updated June 9, 2026 · §202606092046-14 ·

Logs Instrumentation Guide

How to instrument a service for logs on the ShipSolid observability platform. Full platform-side detail: Logging Implementation Guidelines.


How Logs Reach Loki

Application (stdout/stderr)
    → Kubelet log files (/var/log/pods/...)
        → Grafana Alloy DaemonSet (log collection + filter pipeline)
            → Grafana Cloud Loki

The Alloy DaemonSet tails all pod logs automatically. It applies a level-based filter before forwarding:

Namespace typeLevels forwardedLevels dropped
Application namespaceswarn, errortrace, debug, info
Infrastructure namespaceserror onlytrace, debug, info, warn

No code change is needed to enable log collection. Write valid JSON to stdout and Alloy picks it up automatically.


Standard: Structured JSON Logs

One JSON object per line. Required fields:

FieldTypeExample
levelstringwarn, error — standard casing only
messagestring"payment gateway timeout"
timestampstringISO-8601 UTC
trace_idstringOTel trace ID — inject when a span is active
span_idstringOTel span ID — inject when a span is active

Keep high-cardinality detail (request_id, user_id, query parameters) in the log body only — never in Loki stream labels.

.NET 8 / Serilog

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Warning()   // only warn+ reaches Loki in prod
    .WriteTo.Console(new JsonFormatter())
    .Enrich.WithProperty("service.name", "billing-service")
    .CreateLogger();

// Inject trace context (requires OpenTelemetry Serilog sink)
Log.Warning("Downstream timeout {@Details}", new { url, statusCode, latency_ms });

Python

import logging, json, sys
from opentelemetry import trace

def json_log(level, message, **kwargs):
    span = trace.get_current_span().get_span_context()
    payload = {
        "level": level,
        "message": message,
        "trace_id": format(span.trace_id, "032x") if span.is_valid else None,
        "span_id":  format(span.span_id,  "016x") if span.is_valid else None,
        **kwargs,
    }
    print(json.dumps(payload), file=sys.stdout)

Node.js / pino

const pino = require('pino');
const logger = pino({ level: 'warn', formatters: { level: l => ({ level: l }) } });

// trace_id / span_id injected via pino-opentelemetry or manual enrichment
logger.error({ trace_id, span_id, url, status }, 'Upstream error');

AKS — Default Setup (No Configuration Required)

For services in standard application namespaces (*-app, *-api, *-svc), collection is automatic.

Verify logs are reaching Loki:

{namespace="your-namespace"} | json

If logs appear, nothing else is needed. If missing:

  • Confirm the app writes to stdout, not a file.
  • Confirm valid JSON: kubectl logs <pod> | jq .
  • Confirm level is present and uses a standard value.

Debug logs in dev namespaces: Contact the SRE team to add a namespace override in the Alloy config. Debug logs are only permitted in dev — never qa or prod.


Azure Container Apps (ACA)

Logs are collected via the Grafana Alloy sidecar. Follow the ACA Implementation Guidelines.

Required env vars on your container:

OTEL_EXPORTER_OTLP_ENDPOINT: "http://127.0.0.1:4317"   # sidecar
OTEL_SERVICE_NAME: "billing-service"

Labels Alloy Injects Automatically (AKS)

You do not need to add these as fields in your application logs:

LabelSourceExample
namespacePod metadatabilling-prod
podPod metadatabilling-api-7d9f8c-xk2lp
containerPod metadatabilling-api
nodeNode metadataaks-nodepool1-12345-0
envAlloy pipeline configprod
clusterAlloy pipeline configss-aks-prod-eastus

LogQL Examples

# All logs for a namespace
{namespace="billing-prod"} | json

# Filter by level
{namespace="billing-prod"} | json | level = "error"

# Correlate with a trace
{namespace="billing-prod"} | json | trace_id = "4bf92f3577b34da6..."

# Error rate by container
sum by (container) (
  rate({namespace="billing-prod"} | json | level = "error" [5m])
)

Retention Policy

EnvironmentRetention
dev7 days
qa14 days
prod90 days

For compliance-driven longer retention, contact the SRE team.


Troubleshooting

SymptomLikely causeFix
No logs in LokiApp writing to a file, not stdoutUpdate logging config to use stdout handler
Logs appear but level filter ignoredMissing or non-standard level fieldEnsure "level": "warn" — not "lvl", "severity"
INFO in prod unexpectedlyNamespace miscategorised in AlloyRaise with SRE team to fix namespace label
Structured fields not searchablePlain text, not JSONSwitch to JSON formatter
Log volume spike causing cost alertINFO/DEBUG enabled in prodCheck MinimumLevel in app config and framework overrides

Validation Checklist

  • kubectl logs <pod> output is valid JSON (one object per line)
  • Logs appear in Grafana Loki under the correct namespace
  • level field is present and uses a standard value
  • trace_id and span_id are injected when a trace context is active
  • No INFO or lower logs appear in Loki for qa/prod namespaces
  • No PII is present in any log field

Local graph

Full graph →