Log Analysis and Diagnostic Data Gathering Questions
Extracting signal from existing logs and diagnostic output to find a root cause: parsing and querying log data, correlating traces and metrics during an investigation, and gathering the right diagnostic information (including asking clarifying questions) before drawing conclusions. Covers text-processing and query techniques for locating evidence in logs (structured log parsing, ElasticSearch/SQL-style log queries, log aggregation and retention trade-offs) and reconstructing a timeline from the data on hand. This is the analysis-of-existing-data skill used during troubleshooting and investigation across infrastructure and operations roles: distinct from monitoring and observability, which is about instrumenting a system so telemetry exists in the first place (see the observability topics for that), and distinct from SIEM-based security detection and formal digital-forensics practice (chain of custody, artifact/disk/memory analysis), which have their own dedicated coverage elsewhere in the catalog.
Demonstrate practical journalctl usage: give commands and short explanations for the following tasks:
- List all boots and show the last boot's ID
- Show logs for unit nginx.service filtered to warning and above
- Follow logs in real time for a unit
- Export journal entries in JSON for downstream parsing
Also explain the significance of fields like _SYSTEMD_UNIT and _PID in the journal.
Sample Answer
Direct answer
journalctl is the query tool for the systemd journal, the structured binary log store used by most modern Linux distributions alongside (or instead of) flat files in /var/log. The four commands below are standard, documented journalctl usage; they are shown as reference commands rather than a captured run, since this environment has no systemd journal to query against.
Structured elaboration
1) List all boots and show the last boot's ID
journalctl --list-boots
journalctl --list-boots | tail -n 1
--list-boots prints one line per system boot: a relative index (0 is the current boot, -1 the previous one, and so on), the boot ID, and its start/end timestamps. The last line of that output is the most recent boot; its second column is the boot ID you'd pass to journalctl -b <ID>.
2) Show logs for nginx.service filtered to warning and above
journalctl -u nginx.service -p warning
-p (priority) filters by syslog severity. Passing a single level like warning means "this level or anything more severe" (warning, error, critical, alert, emergency), matching "warning and above."
3) Follow logs in real time
journalctl -u nginx.service -f
-f behaves like tail -f: it prints new entries as they're written instead of exiting after the current buffer.
4) Export entries as JSON for downstream parsing
journalctl -u nginx.service -o json
-o json emits one JSON object per line, not json-pretty, which spans multiple lines per entry and is awkward to stream. One-object-per-line is what lets a downstream parser (jq, a Python script, a log shipper) read the output incrementally.
Field significance
_SYSTEMD_UNITidentifies which systemd unit emitted the entry. On a host running many services, this is what lets you filter (journalctl -u <unit>) or, when reading exported JSON, group entries by service without relying on message text._PIDis the originating process ID. It matters when a unit forks multiple worker processes (a pre-fork web server, a pool of workers): grouping by_PIDlets you isolate the specific worker instance a request or crash came from, which you often need when correlating with a process-level diagnostic taken from that same PID.
Worked example
Given a hypothetical JSON export like:
{"_SYSTEMD_UNIT":"nginx.service","_PID":"4213","MESSAGE":"worker process started"}
{"_SYSTEMD_UNIT":"nginx.service","_PID":"4214","MESSAGE":"worker process started"}
{"_SYSTEMD_UNIT":"nginx.service","_PID":"4213","MESSAGE":"connection reset by peer"}
Piping this through jq -r 'select(._PID=="4213") | .MESSAGE' isolates only the events from PID 4213, showing that specific worker hit the reset, not its sibling on PID 4214, a distinction the raw message text alone would not make.
Trade-offs & pitfalls
A common mistake is filtering only by unit and assuming that captures everything relevant. A crash can leave entries under kernel (an out-of-memory kill) or under systemd itself (a unit that failed to even start) that never carry _SYSTEMD_UNIT=nginx.service. When a unit-scoped query comes up empty or incomplete, widen to journalctl -k for the kernel ring buffer, or drop the unit filter for a plain time-window query, before concluding the journal has nothing.
You are given the following snippet from production logs and traces. Analyze the events and identify the most likely root cause and the immediate mitigation steps you would take. Logs:
2025-11-10T10:02:15.101Z service-A trace=abc123 request=R1 status=200 latency_ms=120
2025-11-10T10:02:15.201Z service-B trace=abc123 request=R1 status=500 error="DB timeout"
2025-11-10T10:02:15.301Z service-A trace=abc123 request=R1 status=200 retry=1
2025-11-10T10:02:16.000Z service-C trace=def456 request=R2 status=503
Provide a reasoned RCA hypothesis and at least three concrete verification steps and mitigations.
Sample Answer
Direct answer
Reading the four lines directly: request R1 (trace abc123) called service-B, which returned a 500 with a "DB timeout" error; service-A then retried and got a 200 on the same trace, meaning the client-side retry masked the failure from the caller. A separate request R2 (trace def456) hit service-C and got a 503 with no retry visible. The most likely root cause is a transient database or downstream-dependency slowdown around 10:02:15, severe enough to time out service-B's call and to also affect service-C, whether directly or through a shared dependency; the fact that a retry immediately succeeded on R1 points at a transient condition rather than a hard failure.
Structured elaboration
Rather than eyeballing four lines by hand, group log lines by trace ID programmatically. That scales to a real incident with thousands of lines, and it's the same operation you'd reach for first when actually triaging this:
import re
from collections import defaultdict
LOG = """\
2025-11-10T10:02:15.101Z service-A trace=abc123 request=R1 status=200 latency_ms=120
2025-11-10T10:02:15.201Z service-B trace=abc123 request=R1 status=500 error="DB timeout"
2025-11-10T10:02:15.301Z service-A trace=abc123 request=R1 status=200 retry=1
2025-11-10T10:02:16.000Z service-C trace=def456 request=R2 status=503
"""
KV_RE = re.compile(r'(\w+)=("[^"]*"|\S+)')
def parse_line(line):
ts, service, rest = line.split(" ", 2)
fields = {"ts": ts, "service": service}
for key, val in KV_RE.findall(rest):
fields[key] = val.strip('"')
return fields
events = [parse_line(l) for l in LOG.strip().splitlines()]
by_trace = defaultdict(list)
for e in events:
by_trace[e["trace"]].append(e)
for trace_id, evs in by_trace.items():
evs.sort(key=lambda e: e["ts"])
print(f"trace={trace_id}")
had_error = False
recovered = False
for e in evs:
status = int(e["status"])
tag = ""
if status >= 500:
had_error = True
tag = f" <- ERROR ({e.get('error', 'no error field')})"
if e.get("retry") and status < 500:
recovered = True
tag = " <- succeeded after retry"
print(f" {e['ts']} {e['service']} status={status}{tag}")
verdict = "recovered via client-side retry" if (had_error and recovered) else (
"unresolved failure, no retry observed" if had_error else "no error")
print(f" verdict: {verdict}\n")
Worked example (executed output)
trace=abc123
2025-11-10T10:02:15.101Z service-A status=200
2025-11-10T10:02:15.201Z service-B status=500 <- ERROR (DB timeout)
2025-11-10T10:02:15.301Z service-A status=200 <- succeeded after retry
verdict: recovered via client-side retry
trace=def456
2025-11-10T10:02:16.000Z service-C status=503 <- ERROR (no error field)
verdict: unresolved failure, no retry observed
Grouping by trace ID and looking at each trace's outcome is exactly what separates the two requests here: R1 resolved (error, then a successful retry, one clean causal story), while R2 is still an open, unresolved failure with no evidence of recovery in this snippet.
Verification steps
- Pull database or downstream-dependency metrics (CPU, connection count, query latency) for the
10:02:15to10:02:16window; a timeout error alone doesn't tell you whether the database was overloaded, a specific query was slow, or the network path was degraded. - Check service-B's own logs and metrics for connection pool exhaustion or a spike in outstanding requests right before the timeout, since that would point at capacity rather than the database itself.
- Check whether service-C shares the same downstream dependency as service-B (same database, same connection pool, same upstream service); if so, the two failures likely share one root cause rather than being coincidental.
- Check for a recent deploy or configuration change in the few minutes before
10:02:15; a narrowed connection pool or a newly slow query are common self-inflicted causes of exactly this pattern.
Trade-offs & pitfalls
The tempting mistake is treating R1 as "fine" because it eventually returned 200. A retry that masks a transient failure is still evidence of degradation, and if service-A's retries are not rate-limited, a burst of retries during a real outage can amplify load on the already-struggling dependency, turning a transient blip into a cascading one. The other trap is treating R2's bare 503 as unrelated just because it's a different trace and a different service; two failures in the same one-second window sharing infrastructure are worth checking for a common cause before assuming they're independent.
Write an ElasticSearch DSL query or SQL (pick one and state it) that finds users who experienced more than five HTTP 500 responses within any 10-minute window in the last 24 hours. Assume an index/table 'http_logs' with fields: timestamp (UTC), user_id, status_code, endpoint. Provide the query and explain assumptions about timestamp normalization and log completeness.
Sample Answer
Direct answer
I'll write this in SQL. Self-join http_logs to itself on matching user_id, keep only pairs where the second row's timestamp falls within 10 minutes after the first, group by user and window-start, and keep groups with more than 5 matches. That directly implements "more than five 500s in any 10-minute window" without assuming the windows are aligned to clock boundaries (00:00, 00:10, and so on); a genuinely sliding window has to consider every possible 10-minute span, not just fixed buckets.
Approach
import sqlite3
from datetime import datetime, timedelta, timezone
conn = sqlite3.connect(":memory:")
conn.execute("""
CREATE TABLE http_logs (
timestamp TEXT,
user_id INTEGER,
status_code INTEGER,
endpoint TEXT
)
""")
now = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
def ts(minutes_ago, seconds=0):
return (now - timedelta(minutes=minutes_ago, seconds=-seconds)).strftime("%Y-%m-%d %H:%M:%S")
rows = []
# user 42: six 500s inside one 10-minute window (0..9 min ago) -> should trip
for i, m in enumerate([9, 7, 6, 4, 2, 0]):
rows.append((ts(m), 42, 500, "/checkout"))
# user 99: five 500s spread across 40 minutes -> should NOT trip
for m in [40, 30, 20, 10, 0]:
rows.append((ts(m), 99, 500, "/search"))
# user 7: five 500s inside 10 minutes -> exactly 5, "more than five" excludes it
for m in [8, 6, 4, 2, 0]:
rows.append((ts(m), 7, 500, "/login"))
conn.executemany(
"INSERT INTO http_logs (timestamp, user_id, status_code, endpoint) VALUES (?, ?, ?, ?)",
rows,
)
conn.commit()
query = """
SELECT DISTINCT user_id
FROM (
SELECT a.user_id, a.timestamp AS window_start, COUNT(*) AS error_count
FROM http_logs a
JOIN http_logs b
ON b.user_id = a.user_id
AND b.status_code = 500
AND b.timestamp >= a.timestamp
AND b.timestamp < datetime(a.timestamp, '+10 minutes')
WHERE a.status_code = 500
AND a.timestamp >= datetime(?, '-1 day')
GROUP BY a.user_id, a.timestamp
HAVING COUNT(*) > 5
)
ORDER BY user_id
"""
cursor = conn.execute(query, (now.strftime("%Y-%m-%d %H:%M:%S"),))
print("users with >5 HTTP 500s in any 10-minute window (last 24h):")
for (user_id,) in cursor.fetchall():
print(f" user_id={user_id}")
Worked example (executed output)
users with >5 HTTP 500s in any 10-minute window (last 24h):
user_id=42
Three synthetic users test the boundary explicitly: user 42 has six 500s inside a 10-minute span (correctly flagged), user 7 has exactly five inside 10 minutes (correctly excluded, since the question asks for more than five), and user 99 has five 500s spread across 40 minutes (correctly excluded, since no single 10-minute window contains more than five of them).
Key points
- The self-join with
b.timestamp >= a.timestamp AND b.timestamp < datetime(a.timestamp, '+10 minutes')treats every error event as a candidate window start and counts how many errors (including itself) fall in the next 10 minutes from there; if any such window has more than 5, that user is flagged. DISTINCTin the outer query collapses potentially many qualifying window-starts for the same user down to one row per user, since the question asks which users tripped the threshold, not how many times.- An engine with native window-frame support (PostgreSQL, for example) can express the same sliding-window count more concisely with
COUNT(*) OVER (PARTITION BY user_id ORDER BY timestamp RANGE BETWEEN INTERVAL '10 minutes' PRECEDING AND CURRENT ROW). That is valid, idiomatic Postgres syntax, shown here for comparison only; it was not executed in this environment, so the self-join version above is the one to trust as verified.
Trade-offs & pitfalls
Timestamp normalization: this assumes timestamp is stored consistently in UTC (or at least a single consistent timezone) across all rows; comparing timestamps with mixed offsets would silently produce wrong window boundaries. Log completeness: if http_logs has gaps (a service that failed to log some requests, or a period logs were dropped entirely), this query can only see what's actually in the table and would undercount, never overcount, a user's true error rate. Performance: the self-join is quadratic in the worst case (http_logs rows for one user squared) without an index; in production you'd want an index on (user_id, status_code, timestamp) so both the initial filter and the join lookup are efficient rather than full scans.
After a critical outage you discover that some logs were lost due to a rotation misconfiguration. Describe the forensic investigation steps you would take: evidence preservation, reconstructing timelines, correlating remaining traces and metrics, and long-term fixes to logging and retention to prevent recurrence.
Sample Answer
Direct answer
Treat this like any forensic investigation: preserve what still exists before it decays further, rebuild the timeline from every surviving source (not just the logs that were lost), and only then fix the rotation problem so it cannot happen again. The rotation loss itself becomes part of the incident, not just an inconvenience while investigating the original outage.
Structured elaboration
Evidence preservation (first)
- Immediately copy every surviving log file, rotated archive, and journal export to a separate location before anything else runs rotation, cleanup cron jobs, or disk pressure that could delete more evidence. Treat the current state as read-only.
- Snapshot configuration alongside the data: the
logrotateconfig (logrotateis the standard Linux utility that rotates, compresses, and deletes log files on a schedule) or thejournaldconfig (journaldis systemd's log-collecting daemon) / container logging driver config, any log-shipping agent config, and the retention policy in effect at the time. You will need these to explain the loss, not just to fix it. - Preserve non-log evidence that was not affected by the rotation bug: metrics/dashboards, alert history, deploy and change-management records, and any centralized copy a shipping agent may have already forwarded before local deletion happened.
Reconstructing the timeline from what remains
- Anchor on sources the rotation bug did not touch: monitoring alert timestamps, deploy pipeline timestamps, load balancer or gateway logs (often on a separate host/retention policy), and database slow-query or error logs.
- If a log-shipping agent was running, check whether a central log store already has a copy of what was lost locally; local rotation deleting a file does not delete an already-shipped copy.
- Build the timeline in UTC from the start, and note explicitly which windows have zero surviving log coverage, so the gap itself is documented rather than silently absent from the narrative.
Correlating remaining traces and metrics
- Use metric anomalies (error-rate spikes, latency jumps, restart counts) to bound the incident window even where no log line survives inside it.
- Cross-reference any request or correlation IDs that do survive (in a load balancer log, for instance) against the services you have log coverage for, to partially reconstruct request flow through the gap.
- Where two independent sources agree on a timestamp and a plausible causal link, treat that as higher confidence than a single source alone.
Long-term fixes to logging and retention
- Separate rotation from retention: local rotation should only ever archive or compress, never delete, until a shipping agent has confirmed successful delivery to a durable off-host store.
- Ship logs off the host in near real time so a single host's local rotation bug is not a single point of failure for the evidence.
- Add an explicit alert on log-volume dropping to near zero for a service that is still running; a rotation misconfiguration usually shows up as a sudden, suspicious silence before anyone notices logs are actually gone.
- Periodically test the rotation and retention configuration itself (a scheduled check that logs from N days ago are still retrievable where policy says they should be), rather than only discovering it is broken during an incident.
Worked example
Say the rotation config was set to keep only 2 rotated files at 10 MB each (20 MB total), but the service was logging at a rate that filled 20 MB in under an hour during the incident. By the time anyone started investigating hours later, the relevant window had already rotated out. The forensic reconstruction in this case would lean on the load balancer's access log (a separate host, unaffected), the deploy record showing a config push 12 minutes before the first alert, and the database's own slow-query log, which together bound the incident to a 20-minute window and identify the deploy as the most likely trigger, even though the application's own logs for that window are gone.
Trade-offs & pitfalls
The biggest mistake here is spending the first hour trying to recover the deleted logs (which is usually not possible once rotation has removed and overwritten disk blocks) instead of immediately preserving what remains and pivoting to alternate sources. A second common mistake is fixing only the rotation size/count without addressing the systemic issue: any purely host-local retention policy is one misconfiguration away from doing this again, which is why the durable long-term fix is decoupling deletion from local rotation entirely.
You're investigating a payment-processing incident and need to search production logs for the failing requests, but those logs contain full card numbers and other data covered by PCI and GDPR. How do you get the investigability you need without violating those obligations, and what would you want changed about how these logs are captured so this isn't a recurring problem?
Sample Answer
Direct answer
For the incident in front of you, search on identifiers that don't require touching the sensitive fields at all (request ID, timestamp, customer ID, error code) and only reveal the sensitive data itself through a narrow, audited, time-boxed path if the investigation truly can't proceed without it. Longer term, the real fix is to stop capturing the raw sensitive fields in the first place, so this choice never has to be made mid-incident again.
Framing
PCI (the Payment Card Industry Data Security Standard, the contractual security rules card networks impose on anyone handling card data) and GDPR (the EU's General Data Protection Regulation, which governs how personal data about EU individuals is processed) both restrict access to the sensitive fields, not the existence of logs about the transaction. That distinction is what makes this solvable: you can usually reconstruct "which requests failed, when, for which customer, with what error" using metadata that was never sensitive in the first place. The full card number is rarely what you actually need to diagnose a payment-processing bug; it's collateral damage from logs that captured the whole request body.
Getting investigability now
- Search first on non-sensitive correlation fields: request ID, trace ID, customer/account ID (if that's not itself treated as directly identifying under GDPR in your setup), timestamp range, HTTP status code, and any application-level error code. Most "why did these payments fail" investigations resolve entirely on this metadata, since the failure mode (timeout, declined, validation error) usually shows up in the error code and stack trace, not in the card number itself.
- If the logs are structured (fielded, not raw text blobs), you can query "requests where card_number is present and status=failed" without ever displaying the card_number field's value, only its presence and the surrounding context. That covers a surprising fraction of "I need to search logs containing sensitive fields" requests: you need to select rows by a sensitive field's characteristics, not read the field.
- If you genuinely need to see a real card number (e.g. to confirm with the payment processor which specific transaction failed), that access should go through a narrow, logged, time-boxed reveal path, not a general log search UI: a specific request, tied to an incident ticket, approved, and auditable after the fact (who looked at what, when, and why). This turns "can I see the PAN (primary account number, the full card number)" from a routine query into a rare, accountable exception.
- Whatever you find, don't copy the sensitive value itself into the incident writeup, dashboard, or Slack thread; reference it by a token or the last 4 digits, which is enough for anyone reading the postmortem to identify the transaction without spreading the sensitive value further.
What you'd want changed about capture
- Redact or tokenize sensitive fields at the point of logging, not afterward. If the full card number is in the log at all, you're now relying on every downstream consumer (search index, log shipper, backup, anyone with read access) to also be compliant, which doesn't scale. The application code that writes the log line should already be logging
"pan": "**** **** **** 4242"or a stable token, never the raw number. - For cases where you do need to correlate the same card across events without exposing it, use a deterministic token or a keyed hash (not a reversible encryption you'll be tempted to "just decrypt this once") so you can still ask "did this same card fail three times in an hour" without ever storing the plaintext.
- Push this into a schema/lint check in CI: a log-statement linter or a schema validator that flags any log call touching a known-sensitive field name (
card_number,cvv,ssn) before it ships, so the fix isn't "audit logs after the fact" but "the sensitive value structurally never reaches the log pipeline."
Trade-offs & pitfalls
- Redacting too aggressively can remove the very context you need during an incident; the goal is field-level redaction of the specific sensitive values, not truncating or dropping entire log lines that happen to contain them.
- A reveal path that's too slow or bureaucratic will get bypassed under incident pressure ("just grep it, we'll deal with the audit later"); it needs to be fast enough to actually get used correctly, or people will route around it and you're back where you started.
- Tokenization adds an operational dependency (the token vault) that itself needs to be highly available; if it's down, so is your ability to correlate sensitive events, which is a real cost worth weighing against the compliance benefit.
Unlock Full Question Bank
Get access to all 28 Log Analysis and Diagnostic Data Gathering interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.