Logs vs mavefornemmelse – sådan vinder observability hver gang
Det er som at cykle i mørke uden lys vs med en billig forlygte
At drifte en lille webapp uden observability føles lidt som at cykle gennem byen kl. 23 uden cykellys. Det går ofte fint. Indtil det ikke gør.
Du kan godt ane hullerne i vejen. Du kan nogenlunde gætte dig til, hvornår der kommer en bil. Men når noget går galt, er du mest af alt bare heldig, hvis du ikke rammer noget hårdt.
En fuld enterprise observability-stak er så den der 10.000 kroners gravel-cykel med dynamolys, navigation og alt muligt. Flot, men overkill, hvis du bare skal ned i Netto.
Det her handler om den billige, men solide forlygte: et minimum observability-setup til små webapps, hvor du kan svare på spørgsmålet: “Hvad skete der lige?” på 2 minutter i stedet for 2 timer.
Målet: 2 minutters svar på “hvad skete der”
Jeg vil starte baglæns. Forestil dig du får beskeden:
“Login virker ikke. Jeg trykker, og der sker bare ikke noget.”
Om 2 minutter vil du gerne kunne svare nogenlunde præcist:
- Hvor mange fejlede requests har der været på /login de sidste 10 minutter?
- Er det alle brugere eller kun nogle bestemte?
- Ser du konkrete fejl i backend (stacktraces, timeouts, 500s)?
- Er noget pludselig blevet meget langsomt (database, ekstern API osv.)?
Det kræver ikke en kæmpe platform. Det kræver:
- Structured logging med korrelation-id (request_id)
- Få, men gode metrics på latency, fejlrate og belastning
- Traces i de tilfælde hvor du har flere services eller eksterne kald
- Nogle få alarmer som peger dig i den rigtige retning uden at vække dig hver nat
Det er det setup, jeg gennemgår. Ikke teori for teorien skyld, men hvad der rent faktisk hjælper dig, når en rigtig bruger sidder fast.
Logging vs structured logging – forskellen mellem støj og svar
De fleste starter med logging sådan her:
console.log("User logged in");
console.log("Error in payment step");
Og det er fint. Lige indtil:
- Du får 500 linjer “Error in payment step” uden kontekst
- Du ikke kan se, hvilke fejl hører til hvilken request
- Du ikke kan filtrere logs efter bruger, route, statuskode osv.
Structured logging betyder, at dine logs ikke bare er tekst, men datafelter, du kan søge i. Typisk JSON.
Et minimum log-event for en webrequest
Hvis du kun skulle logge én ting for hver request, ville jeg logge et event når requesten er afsluttet:
{
"timestamp": "2026-05-14T19:12:33.123Z",
"level": "info",
"message": "request_completed",
"request_id": "b9c1d6e3-1a2b-4b8b-8c9e-22a1f7a4e1f3",
"method": "POST",
"route": "/api/login",
"status_code": 200,
"duration_ms": 123,
"user_id_hash": "u_9f13a7",
"service": "auth-api",
"env": "production"
}
Det her ene log-event gør allerede overraskende meget:
- Du kan filtrere på
route+status_codeog se fejl - Du kan søge på
request_idhvis du vil følge én request - Du kan se, om noget pludselig er blevet langsomt i
duration_ms - Du kan se, om det er én bestemt bruger (
user_id_hash) der rammer fejl
Typiske felter, der betaler sig
Hvis vi skal lave et minimalt, men brugbart “schema” for dine web-logs, ville jeg have:
- request_id: et unikt id per request (UUID eller lign.)
- method: GET, POST osv.
- route: normaliseret route, f.eks.
/users/:idi stedet for/users/123 - status_code: HTTP status
- duration_ms: hvor lang tid tog requesten
- user_id_hash: hash eller anonymiseret bruger-id
- service: navn på din app/service
- env: prod, staging, dev
Du kan altid udvide, men de her felter alene vil redde dig mange gange. Jeg stoler efterhånden mere på sådan nogle logs end på min egen hukommelse, og det er lidt samme pointe som i artiklen om netop at bruge logs som din hukommelse.
Fejl-logs: stacktrace + kontekst
Når noget fejler, skal du både have en menneskeligt læsbar besked og nok maskin-kontekst til at kunne gruppere fejlene.
{
"timestamp": "2026-05-14T19:12:33.456Z",
"level": "error",
"message": "failed_to_call_payment_provider",
"request_id": "b9c1d6e3-1a2b-4b8b-8c9e-22a1f7a4e1f3",
"route": "/api/checkout",
"user_id_hash": "u_9f13a7",
"error_type": "TimeoutError",
"error_message": "Request timed out after 3000ms",
"stack": "TimeoutError: ...",
"service": "payments-api",
"env": "production"
}
Typisk fejl: du logger kun error.message og glemmer stacktracen. Når du så endelig vil debugge, kan du ikke se hvor i koden det gik galt.
Correlation-id: sådan binder du ting sammen
Et request_id (ofte kaldet correlation-id) skal føles kedeligt at snakke om, men det gør din hverdag markant mindre kaotisk.
Flowet er:
- Indgående request får et nyt
request_id(eller bruger et eksisterende header, f.eks.x-request-id) - Du sender
request_idvidere til andre services og eksterne API-kald som header - Alle logs for den request indeholder samme
request_id
Så kan du:
- Søge i dine logs på præcis én request
- Se kæden: frontend request → backend → ekstern API
Metrics vs logs – hvornår du har brug for tal i stedet for tekst
Logs er gode til at svare på: “Hvad skete der med den her request?”
Metrics er gode til: “Er noget generelt ved at skride?”
Hvis jeg skulle vælge et absolut minimum metrics-setup til en lille webapp, ville jeg tracke tre grupper:
- Traffic: hvor meget der sker
- Errors: hvor ofte det går galt
- Latency: hvor hurtigt det opleves
De 7 metrics der opdager problemer, før brugerne skriver til dig
Her er et godt start-sæt:
- http_requests_total (counter)
Antal HTTP requests pr. route + status-code. - http_requests_errors_total (counter)
Antal 5xx-errors pr. route. - http_request_duration_ms (histogram)
Latency pr. route. Interessant i percentiler, f.eks. p50, p90, p99. - db_query_duration_ms (histogram)
Hvis du har database. Giver dig hurtigt syn for, om det er databasen der driller. - external_api_failures_total (counter)
Fejlrate mod en eller to kritiske eksterne services. - worker_queue_length (gauge)
Hvis du har en job-queue. Viser om baggrundsarbejde hober sig op. - cpu_usage / memory_usage (gauge)
Fra hostingplatformen. Du behøver ikke fin-tune, bare se om det eksploderer.
Et lille Node-eksempel med metrics
Et ultra-forsimplet eksempel med Prometheus metrics i en Express-app:
import express from "express";
import client from "prom-client";
const app = express();
const register = new client.Registry();
const httpRequestDuration = new client.Histogram({
name: "http_request_duration_ms",
help: "Duration of HTTP requests in ms",
labelNames: ["route", "method", "status_code"],
buckets: [50, 100, 200, 500, 1000, 2000]
});
register.registerMetric(httpRequestDuration);
client.collectDefaultMetrics({ register });
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
httpRequestDuration.labels(req.path, req.method, res.statusCode).observe(duration);
});
next();
});
app.get("/metrics", async (req, res) => {
res.set("Content-Type", register.contentType);
res.end(await register.metrics());
});
app.listen(3000);
Det ser lidt teknisk ud, men pointen er enkel: du får et endpoint, som din metrics-løsning (Prometheus, hostingens indbyggede, hvad end du bruger) kan scrape.
Tracing vs ingen tracing – hvornår du faktisk har brug for det
Tracing er det sted, hvor mange små teams står af, fordi det virker stort. Men distributed tracing giver dig noget, som hverken logs eller metrics alene kan:
- En visuel kæde af, hvad der sker i en request
- Tid brugt i hvert hop: web → service A → database → ekstern API
- Sammenhæng på tværs af services via trace-id/span-id
Jeg plejer at sige det sådan her:
- Har du kun én backend-service + en database? Du kan sagtens leve uden tracing i starten.
- Har du flere services, eller hård afhængighed af eksterne APIer? Så begynder tracing at give mening meget hurtigere.
Hvad et trace faktisk viser
Forestil dig et login-flow:
- Span 1: HTTP POST /login (web)
- Span 2: SELECT user FROM users WHERE email = ? (db)
- Span 3: POST /check-password (ekstern auth-service)
På et trace kan du se, at:
- Span 1 tog 1.200 ms
- Span 2 tog 20 ms
- Span 3 tog 1.150 ms og fejlede med timeout
I stedet for bare at se “login er langsomt” i logs eller metrics, kan du pege direkte på “det er auth-service kaldet der er problemet”. Det er især guld værd i lidt større full stack setups, hvor mange lag er inde over.
OpenTelemetry vs hjemmerullet – hvorfor du ikke skal opfinde dit eget format
OpenTelemetry (OTel) er i praksis bare en standard og nogle SDKs til:
- Logs
- Metrics
- Traces
Alle de store observability-værktøjer forstår OTel-formatet. Det betyder, at du kan:
- Instrumentere din kode én gang
- Skifte backend senere (selfhost, vendor A, vendor B)
Hvis du er vant til at rode med devtools og debugging, er det samme vibe som i artiklen om at holde op med at debugge i blinde, bare på servicelag i stedet for browseren.
Minimal OTel-opsætning i en Node-webapp
Et meget forsimplet eksempel (pseudo-ish kode) for at vise strukturen:
// instrumentation.js
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
});
export const sdk = new NodeSDK({
traceExporter,
instrumentations: [getNodeAutoInstrumentations()]
});
// index.js
import { sdk } from "./instrumentation.js";
sdk.start()
.then(() => {
// start din Express-app her
})
.catch(err => {
console.error("Failed to start OTel SDK", err);
});
Pointen er ikke, at du skal kunne det hele uden docs. Pointen er:
- Du har én SDK der indsamler telemetry
- Du har en exporter der sender data til et endpoint
- Du kan pege den exporter et nyt sted hen senere uden at ændre din app-logik
Faldgruber med OpenTelemetry i små projekter
De klassiske ting jeg selv har rodet med:
- Du får for mange traces. Brug sampling (f.eks. 10 %) i prod.
- Du glemmer at sætte
service.name, så alt hedder “unknown_service”. - Du sender traces over almindeligt internet uden TLS. Brug https/secure endpoints.
Alarmer vs alarm-træthed – hvad der faktisk skal bippe
Nu kommer den del, der enten gør dig tryg eller træt: alarmer.
Målet er ikke, at alt sender en Slack-besked. Målet er, at du får besked, når:
- Brugere ikke kan bruge de vigtigste funktioner
- Systemet er på vej ud over kanten (fejlrate eller ressourcespikes)
Start med tre alarmer
For en lille webapp ville jeg starte med:
- Høj fejlrate på vigtig route
Eksempel: mere end 2 % 5xx på/checkoutover 5 minutter. - Ekstrem latency på vigtig route
Eksempel: p95 latency > 2 sekunder på/loginover 10 minutter. - Ressource-bottleneck
Eksempel: CPU > 90 % i 10 minutter, eller database-connection pool konstant fuld.
Alt andet kan du se på dashboards, når du aktivt kigger.
Fejlbudget light til små apps
Hvis du vil undgå at skifte mellem “ingen alarmer” og “alt brænder” kan du tænke i et mini-fejlbudget:
- Accepter f.eks. at checkout må fejle op til 1 % af tiden
- Hvis du over det, er det “incident” og skal fikses snart
- Hvis du under det, kan du leve med små glitch
Det behøver ikke være en tung SRE-proces. Det er bare en måde at sige: “Vi ved godt, ting ikke er perfekte, men vi ved også, hvornår det er for meget.”
Værktøjsvalg: open source vs hosted – hvad giver mening som lille app
Her bliver der tit kastet mange navne rundt. Jeg vil skære det ned til, hvad jeg faktisk ser små teams bruge:
Til logs
- Selfhostet: Loki + Grafana, eller Elastic stack (Elasticsearch + Kibana). Mere arbejde, mere kontrol.
- Hosted: Logtail, BetterStack, Papertrail, Datadog, mange flere. Typisk let at komme i gang, men pas på pris hvis du logger alt.
Uanset løsning: log JSON og send via en log transport (agent, http, hvad der passer til din stack). Undgå raw console.log direkte til fil i prod, hvis du kan slippe for det.
Til metrics
- Prometheus + Grafana er standarden i open source-verdenen
- Mange hosting-løsninger (f.eks. cloud platforms) har indbyggede metrics og simple alarmer
Hvis din app er lille og ligger på noget som Vercel, Render eller Heroku, kan du ofte komme langt med deres egne dashboards i starten. Du behøver ikke altid hele Observatorium fra dag 1.
Til tracing
- Jaeger eller Tempo (selfhost)
- De fleste vendors understøtter OTel-traces direkte
Hvis du kun lige er begyndt at overveje tracing, så tænk sådan her:
- Start med logs + metrics
- Når du har flere services eller oplever “spøgelses-latency”, så tag tracing ind
Incident mini-runbook – fra alarm til “vi ved hvad der skete”
Nu til den del, hvor det hele skal hænge sammen. Forestil dig en rigtig situation.
Slack besked:
“Alarm: 5xx rate > 5 % på /api/checkout de sidste 5 minutter”
1. Bekræft at problemet er rigtigt
Første skridt: tjek metrics-dashboard.
- Er fejlraten stadig høj?
- Er der også spike i latency eller CPU?
Hvis det var en kort spike, der allerede er væk, kan du notere det, men måske ikke kaste alt, hvad du har i hænderne. Hvis den stadig er høj, går du videre.
2. Find et konkret eksempel via logs
Filtrer logs på:
route = "/api/checkout"status_code >= 500- tidsinterval: de sidste 10-15 minutter
Tag én af de fejl-requests:
- Notér
request_idoguser_id_hash - Find tilhørende error-log med samme
request_id
Nu har du typisk en stacktrace og en error-type. Du er allerede meget tættere på sandheden end med “checkout virker ikke”.
3. Se mønsteret
Spørg dig selv:
- Er det samme fejltype hver gang, eller flere forskellige?
- Er det alle brugere eller kun nogle (samme
user_id_hasheller region)? - Er latency høj lige før fejlen, eller fejler den hurtigt?
Hvis du har tracing på, kan du her åbne et trace for et af de fejlende request_id og se, hvilket span der er problemet.
4. Beslut: hotfix nu eller senere?
Med de informationer kan du træffe en fornuftig beslutning:
- Er det en klar regression i ny deploy? Rul tilbage.
- Er det et eksternt system, der er nede? Lav midlertidig feature-flag/fallback.
- Er det én bestemt kant-case? Skriv det op som bug, men måske ikke midt om natten.
Uden observability gætter du. Med logs + metrics (+ eventuelt tracing) kan du dokumentere beslutningen.
5. Efter du har slukket branden
Når roen er tilbage, er der to spørgsmål, der giver mening:
- Hvilket signal kunne have vist os problemet tidligere?
- Hvilke felter manglede vi i logsene for at forstå det hurtigere?
Ofte er svaret “vi havde kun en halvdårlig tekstlog”. Så kan du målrettet forbedre dit schema næste dag, i stedet for at opfinde noget abstrakt observability-strategi.
Sådan starter du: et minimums-setup på en aften
Hvis jeg skulle sætte noget i gang på en enkelt aften til en lille Node- eller Python-webapp, ville jeg gøre det her:
Trin 1 – structured logs i stedet for rå console.log
Vælg en logger der kan outputte JSON (pino, winston, structlog osv.).
Lav en lille helper der sikrer, at alle request-logs har:
request_idroutestatus_codeduration_ms
Start med bare at sende logs til stdout og lad din hosting samle dem op. Senere kan du sende dem til et rigtigt logsystem.
Trin 2 – 2-3 vigtige metrics
Implementer:
http_request_duration_mshistogram pr. route- En counter for 5xx-errors pr. route
Eksponer dem på et /metrics-endpoint eller brug din platform, hvis den giver dig noget lignende ud af boksen.
Trin 3 – én simpel alarm
Sæt én alarm op:
- 5xx-rate > f.eks. 2-5 % på din primære API eller vigtigste route
Send den til Slack, mail eller hvor du nu er.
Trin 4 – skriv 5 linjer runbook
Skriv en lille README-sektion i repoet:
Hvis 5xx-alarm går i gang:
1) Tjek dashboard: se fejlrate og latency.
2) Filtrer logs på route + 5xx for sidste 15 min.
3) Tag et request_id og find tilhørende error-log.
4) Vurder: rollback, feature-flag eller bug-ticket.
Det lyder banalt, men når man sidder med puls 120 og en sur kunde, er det rart ikke også at skulle opfinde proceduren.
Hvis du kun ændrer én ting efter det her
Hvis du kun gør én ting anderledes efter du har læst det her, så stop med at smide tilfældige tekstbidder i dine logs.
Begynd at logge færre ting, men med faste felter: request_id, route, status_code og duration_ms. Det alene flytter dig fra “jeg gætter mig frem” til “jeg kan ret hurtigt se, hvad der skete”. Og så kan du bygge metrics, tracing og alarmer ovenpå i det tempo, din lille webapp og din nattesøvn kan klare.









1 kommentar