Log Strategy
Most teams either log too little to investigate anything or too much to find anything (and end up storing PII they didn't mean to). This skill is the middle path: enough to support incident response, not so much that the log becomes a liability of its own.
Pairs with incident-response (where logs get used) and gdpr-technical-controls (where log privacy lives).
When to invoke
- Starting a new service or workflow
- Investigation revealed missing fields ("we don't know what request triggered this")
- Logs are leaking PII or secrets
- Log volume is becoming expensive / unwieldy
- Consolidating to a central logging stack (Loki, ELK, OpenObserve, hosted)
- After an incident where the log was insufficient
Three classes of log, three retention tiers
Treat your logs as three distinct streams. Conflating them is where the trouble starts.
| Class |
Purpose |
Typical retention |
Storage tier |
| Operational |
Debugging, performance, errors |
7–30 days |
Hot index |
| Access |
Who hit what, when (webserver + app request log) |
30–90 days |
Hot + warm |
| Audit |
Security-relevant events (auth, permission changes, sensitive actions) |
12 months+ |
Append-only, immutable where possible |
Costs and tools differ per class. Mixing them gives the audit log's slow-and-expensive retention to the operational stream and is the most common reason logging gets killed by finance.
Operational logs — what to capture
Structured logging beats freeform every time. Choose JSON (or logfmt) and a single library for the whole app.
// pino example — Node
import pino from 'pino';
const log = pino({
level: process.env.LOG_LEVEL ?? 'info',
redact: {
paths: ['password', 'token', 'authorization', '*.password', '*.token', 'req.headers.authorization', 'req.headers.cookie'],
censor: '[REDACTED]',
},
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
requestId: req.id,
// explicitly NOT: headers (except a few safe ones), body
}),
err: pino.stdSerializers.err,
},
});
log.info({ orderId, userId: anonymize(userId) }, 'order created');
log.warn({ requestId, latencyMs }, 'slow query');
log.error({ requestId, err }, 'unhandled error in handler');
Patterns:
- One event per line, structured. Greppable, parseable by any log system.
- A
requestId / traceId on every log line within a request — lets you reconstruct a request's full path. Generate at the edge (Nginx / Cloudflare / app middleware), pass through to downstream services.
- Log levels mean something.
error = an unhandled exception or a degraded user outcome; warn = a recoverable surprise; info = state changes worth knowing about; debug = noisy diagnostics that ship off by default.
- No secrets, no PII — redact in the serializer, not at the call site (humans forget; serializers don't).
- Bounded volume per request — a handler logging 50 lines per request creates a $/month problem. Tighten before launch.
Access logs — webserver + app
Webserver-level access logs (nginx, Caddy, Cloudflare logs) are usually fine with their defaults plus a few tweaks:
# nginx — log format that includes useful diagnostic + minimal PII
log_format main escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",' # consider hashing — see below
'"request_id":"$request_id",'
'"method":"$request_method",'
'"path":"$request_uri",'
'"status":$status,'
'"body_bytes_sent":$body_bytes_sent,'
'"referer":"$http_referer",'
'"user_agent":"$http_user_agent",'
'"request_time":$request_time,'
'"upstream_response_time":"$upstream_response_time"'
'}';
access_log /var/log/nginx/access.log main;
Considerations:
- IPs in DACH/EU — full IP storage is widely treated as personal data. Options:
- Hash with HMAC + rotating key (anonymous but consistent within rotation window)
- Truncate to
/24 for IPv4, /64 for IPv6 (lossy but acceptable for most analytics)
- Keep full only for the abuse-investigation tier with shorter retention (e.g. 7 days)
- No bodies in access logs. Bodies leak passwords on login routes, payment data on checkout, PII on profile updates. The webserver default doesn't log bodies; don't be tempted by "let's just log the request body for debugging".
- Cookies and
Authorization header stripped — they include session identifiers and tokens.
Audit logs — security-relevant events
These are the events you want to find six months from now during an investigation.
What deserves an audit-log entry:
- Authentication: successful login, failed login, MFA challenge result, password change, password reset request, password reset use, account locked, email change request, email change confirm, account deletion
- Authorization: role/permission granted or revoked, admin actions affecting other users, sensitive data export (SAR), data deletion
- Configuration: feature-flag flips, security-setting changes, API key issued/revoked, integration enabled/disabled
- Sensitive data access: who viewed which PII record, who exported the database, who downloaded an attachment containing PII
- Money-touching: refunds issued, manual ledger adjustments, fee waivers, balance transfers
Per entry, capture:
{
"ts": "2026-05-12T14:23:00Z",
"actor": { "type": "user", "id": "u_abc", "via": "session" },
"action": "user.password.reset.request",
"target": { "type": "user", "id": "u_abc" },
"context": {
"ip_hash": "...",
"user_agent": "...",
"request_id": "...",
"result": "ok"
}
}
Patterns:
- Append-only. Use a write-only database role, an append-only S3 bucket with object lock, or a managed audit-log service. The app's normal role must not be able to delete or update audit rows.
- Tamper-evidence for high-stakes contexts: hash-chain each entry against the previous one, or write to a service that does (e.g. AWS QLDB, Tigris).
- Separate storage from operational logs — different retention, different access controls.
- Indexed by actor, target, and action for investigation queries.
- Visible to the user for their own audit trail ("recent activity" view) — surfaces own compromises faster than you do.
What NEVER to log
A handful of values that turn the log into a new liability:
- Passwords, password hashes, password reset tokens
- Session cookies /
Authorization headers / API keys / JWTs / OAuth tokens
- Credit card numbers, CVVs, bank account numbers (PCI scope explodes if you do)
- Private keys, signing keys, encryption keys
- Full social security / national-ID numbers
- Full email or phone of users when an identifier alone would do
- Raw request/response bodies on any auth, payment, or profile endpoint
If a regex / structured-log redactor catches these, you're protected when a future code change accidentally tries to log one.
Centralization choices
Pick a stack and stick to it. Mixing is operational hell.
Self-hosted options:
- Grafana Loki + Promtail / Vector — store JSON logs in object storage, query in Grafana. Cheap, simple, fine for small/medium scale.
- OpenSearch / ELK — full text + dashboarding. More moving parts.
- OpenObserve — newer, integrates logs+metrics+traces, lower operational footprint than ELK.
Hosted options:
- Datadog / New Relic / Honeycomb — turn-key, expensive at volume, easiest pickup
- Better Stack / Axiom / Baselime — affordable mid-tier
- Cloudflare Logpush — if you're already on Cloudflare, ship to R2/S3 cheaply
For audit logs specifically, consider a separate destination (immutable bucket, or a service like AWS CloudTrail / Google Cloud Audit Logs equivalent).
Alerts vs logs
Logs are for investigation; alerts are for getting woken up. They should not be the same thing.
- From operational logs: 5xx rate per service, error-class spike, slow-query rate, queue depth
- From access logs: spike in 4xx per route (especially 401/403/404), spike in 429, unusual ASNs
- From audit logs: admin actions on other users, sensitive data exports, password-reset bursts on a single account, MFA-disabled events
Route each alert class to a different channel — paged for ops, weekly digest for audit anomalies, etc. Reduces alert fatigue.
Cost control
Logs scale linearly with traffic; cost surprises are universal.
- Cap volume at the source. A handler emitting 50 log lines per request is the most common cause of bills.
- Sample debug-level logs in production. Keep 1–10% of
debug and trace.
- Drop bot traffic at the ingest layer if it's clean (otherwise it dominates the access log).
- Retain warm tier short, archive long. Most queries hit the last 7 days; cold storage in S3/R2 is pennies vs hot indexing.
- Reduce field count. Every JSON key in every log line costs at index time. Audit which fields are queried, drop the rest.
Privacy checklist (DACH / EU)
What this skill will not do
- Recommend logging request bodies on auth or payment routes
- Help build a logging stack for systems you do not own
- Replace a regulated-environment (HIPAA / PCI / SOC 2) audit-log requirement — those have specifics this doesn't cover
1---2name: log-strategy3description: Design logging that supports investigations without becoming a privacy liability. Covers what to log and what never to log (PII, secrets), structured logging, retention tiers, centralization choices, alert routing, and the operational-versus-access-versus-audit log split. Invoke when starting a new service, when investigation revealed missing log fields, or when log volume is becoming expensive.4---5
6# Log Strategy
7
8Most teams either log too little to investigate anything or too much to find anything (and end up storing PII they didn't mean to). This skill is the middle path: enough to support incident response, not so much that the log becomes a liability of its own.
9
10Pairs with [`incident-response`](../incident-response/SKILL.md) (where logs get used) and [`gdpr-technical-controls`](../gdpr-technical-controls/SKILL.md) (where log privacy lives).
11
12## When to invoke
13
14- Starting a new service or workflow
15- Investigation revealed missing fields ("we don't know what request triggered this")
16- Logs are leaking PII or secrets
17- Log volume is becoming expensive / unwieldy
18- Consolidating to a central logging stack (Loki, ELK, OpenObserve, hosted)
19- After an incident where the log was insufficient
20
21## Three classes of log, three retention tiers
22
23Treat your logs as three distinct streams. Conflating them is where the trouble starts.
24
25| Class | Purpose | Typical retention | Storage tier |
26|---|---|---|---|
27| **Operational** | Debugging, performance, errors | 7–30 days | Hot index |
28| **Access** | Who hit what, when (webserver + app request log) | 30–90 days | Hot + warm |
29| **Audit** | Security-relevant events (auth, permission changes, sensitive actions) | 12 months+ | Append-only, immutable where possible |
30
31Costs and tools differ per class. Mixing them gives the audit log's slow-and-expensive retention to the operational stream and is the most common reason logging gets killed by finance.
32
33## Operational logs — what to capture
34
35Structured logging beats freeform every time. Choose JSON (or logfmt) and a single library for the whole app.
36
37```ts
38// pino example — Node
39import pino from 'pino';
40
41const log = pino({
42 level: process.env.LOG_LEVEL ?? 'info',
43 redact: {
44 paths: ['password', 'token', 'authorization', '*.password', '*.token', 'req.headers.authorization', 'req.headers.cookie'],
45 censor: '[REDACTED]',
46 },
47 serializers: {
48 req: (req) => ({
49 method: req.method,
50 url: req.url,
51 requestId: req.id,
52 // explicitly NOT: headers (except a few safe ones), body
53 }),
54 err: pino.stdSerializers.err,
55 },
56});
57
58log.info({ orderId, userId: anonymize(userId) }, 'order created');
59log.warn({ requestId, latencyMs }, 'slow query');
60log.error({ requestId, err }, 'unhandled error in handler');
61```
62
63Patterns:
64
65- **One event per line, structured**. Greppable, parseable by any log system.
66- **A `requestId` / `traceId` on every log line** within a request — lets you reconstruct a request's full path. Generate at the edge (Nginx / Cloudflare / app middleware), pass through to downstream services.
67- **Log levels mean something**. `error` = an unhandled exception or a degraded user outcome; `warn` = a recoverable surprise; `info` = state changes worth knowing about; `debug` = noisy diagnostics that ship off by default.
68- **No secrets, no PII** — redact in the serializer, not at the call site (humans forget; serializers don't).
69- **Bounded volume per request** — a handler logging 50 lines per request creates a $/month problem. Tighten before launch.
70
71## Access logs — webserver + app
72
73Webserver-level access logs (nginx, Caddy, Cloudflare logs) are usually fine with their defaults plus a few tweaks:
74
75```nginx
76# nginx — log format that includes useful diagnostic + minimal PII
77log_format main escape=json '{'
78 '"time":"$time_iso8601",'
79 '"remote_addr":"$remote_addr",' # consider hashing — see below
80 '"request_id":"$request_id",'
81 '"method":"$request_method",'
82 '"path":"$request_uri",'
83 '"status":$status,'
84 '"body_bytes_sent":$body_bytes_sent,'
85 '"referer":"$http_referer",'
86 '"user_agent":"$http_user_agent",'
87 '"request_time":$request_time,'
88 '"upstream_response_time":"$upstream_response_time"'
89'}';
90access_log /var/log/nginx/access.log main;
91```
92
93Considerations:
94
95- **IPs in DACH/EU** — full IP storage is widely treated as personal data. Options:
96 - Hash with HMAC + rotating key (anonymous but consistent within rotation window)
97 - Truncate to `/24` for IPv4, `/64` for IPv6 (lossy but acceptable for most analytics)
98 - Keep full only for the abuse-investigation tier with shorter retention (e.g. 7 days)
99- **No bodies in access logs**. Bodies leak passwords on login routes, payment data on checkout, PII on profile updates. The webserver default doesn't log bodies; don't be tempted by "let's just log the request body for debugging".
100- **Cookies and `Authorization` header** stripped — they include session identifiers and tokens.
101
102## Audit logs — security-relevant events
103
104These are the events you want to find six months from now during an investigation.
105
106What deserves an audit-log entry:
107
108- **Authentication**: successful login, failed login, MFA challenge result, password change, password reset request, password reset use, account locked, email change request, email change confirm, account deletion
109- **Authorization**: role/permission granted or revoked, admin actions affecting other users, sensitive data export (SAR), data deletion
110- **Configuration**: feature-flag flips, security-setting changes, API key issued/revoked, integration enabled/disabled
111- **Sensitive data access**: who viewed which PII record, who exported the database, who downloaded an attachment containing PII
112- **Money-touching**: refunds issued, manual ledger adjustments, fee waivers, balance transfers
113
114Per entry, capture:
115
116```json
117{
118 "ts": "2026-05-12T14:23:00Z",
119 "actor": { "type": "user", "id": "u_abc", "via": "session" },
120 "action": "user.password.reset.request",
121 "target": { "type": "user", "id": "u_abc" },
122 "context": {
123 "ip_hash": "...",
124 "user_agent": "...",
125 "request_id": "...",
126 "result": "ok"
127 }
128}
129```
130
131Patterns:
132
133- **Append-only.** Use a write-only database role, an append-only S3 bucket with object lock, or a managed audit-log service. The app's normal role must not be able to delete or update audit rows.
134- **Tamper-evidence** for high-stakes contexts: hash-chain each entry against the previous one, or write to a service that does (e.g. AWS QLDB, Tigris).
135- **Separate storage** from operational logs — different retention, different access controls.
136- **Indexed by actor, target, and action** for investigation queries.
137- **Visible to the user** for their own audit trail ("recent activity" view) — surfaces own compromises faster than you do.
138
139## What NEVER to log
140
141A handful of values that turn the log into a new liability:
142
143- Passwords, password hashes, password reset tokens
144- Session cookies / `Authorization` headers / API keys / JWTs / OAuth tokens
145- Credit card numbers, CVVs, bank account numbers (PCI scope explodes if you do)
146- Private keys, signing keys, encryption keys
147- Full social security / national-ID numbers
148- Full email or phone of users when an identifier alone would do
149- Raw request/response bodies on any auth, payment, or profile endpoint
150
151If a regex / structured-log redactor catches these, you're protected when a future code change accidentally tries to log one.
152
153## Centralization choices
154
155Pick a stack and stick to it. Mixing is operational hell.
156
157**Self-hosted options**:
158
159- **Grafana Loki + Promtail / Vector** — store JSON logs in object storage, query in Grafana. Cheap, simple, fine for small/medium scale.
160- **OpenSearch / ELK** — full text + dashboarding. More moving parts.
161- **OpenObserve** — newer, integrates logs+metrics+traces, lower operational footprint than ELK.
162
163**Hosted options**:
164
165- **Datadog / New Relic / Honeycomb** — turn-key, expensive at volume, easiest pickup
166- **Better Stack / Axiom / Baselime** — affordable mid-tier
167- **Cloudflare Logpush** — if you're already on Cloudflare, ship to R2/S3 cheaply
168
169For audit logs specifically, consider a separate destination (immutable bucket, or a service like AWS CloudTrail / Google Cloud Audit Logs equivalent).
170
171## Alerts vs logs
172
173Logs are for investigation; alerts are for getting woken up. They should not be the same thing.
174
175- **From operational logs**: 5xx rate per service, error-class spike, slow-query rate, queue depth
176- **From access logs**: spike in 4xx per route (especially 401/403/404), spike in 429, unusual ASNs
177- **From audit logs**: admin actions on other users, sensitive data exports, password-reset bursts on a single account, MFA-disabled events
178
179Route each alert class to a different channel — paged for ops, weekly digest for audit anomalies, etc. Reduces alert fatigue.
180
181## Cost control
182
183Logs scale linearly with traffic; cost surprises are universal.
184
185- **Cap volume at the source.** A handler emitting 50 log lines per request is the most common cause of bills.
186- **Sample debug-level logs in production.** Keep 1–10% of `debug` and `trace`.
187- **Drop bot traffic at the ingest layer** if it's clean (otherwise it dominates the access log).
188- **Retain warm tier short, archive long.** Most queries hit the last 7 days; cold storage in S3/R2 is pennies vs hot indexing.
189- **Reduce field count.** Every JSON key in every log line costs at index time. Audit which fields are queried, drop the rest.
190
191## Privacy checklist (DACH / EU)
192
193- [ ] No bodies of POST/PUT/PATCH in access logs
194- [ ] IPs hashed/truncated, or kept full only on a separate stream with short retention
195- [ ] `Authorization`, `Cookie`, `Set-Cookie` headers stripped
196- [ ] Known-sensitive JSON fields redacted in structured logs by key name
197- [ ] Retention period documented per log stream, matches privacy policy
198- [ ] Sub-processor list updated if logs ship to a hosted service
199- [ ] Audit logs separate from operational, with separate retention and access controls
200- [ ] Log queries by employees are themselves audited (who searched what)
201
202## What this skill will not do
203
204- Recommend logging request bodies on auth or payment routes
205- Help build a logging stack for systems you do not own
206- Replace a regulated-environment (HIPAA / PCI / SOC 2) audit-log requirement — those have specifics this doesn't cover