Computing Foundations › Systems Foundations: Storage, Observability, and Tooling › Day 40
Hands-on lab — Day 40: Observability: Logs, Metrics, Traces, and Dashboards
- ← Back to the Day 40 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards/
Commands
Setup
cd labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards Run
bash examples/observe.sh
bash starter/observe.sh Test
bash tests/run_tests.sh File tree
examples/observe.sh expected-output/FIELDS.md expected-output/sample-run.txt expected-output/test-run.txt metadata.yml README.md requirements/README.md security.md starter/observability-worksheet.md starter/observe.sh tests/run_tests.sh troubleshooting.md
Lab README
Day 040 lab — Build a Mini Observability Pipeline
Lesson
- Lesson title: Observability: Logs, Metrics, Traces, and Dashboards
- Day number: 40 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-040-observability-logs-metrics-traces-and-dashboards
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-040-observability-logs-metrics-traces-and-dashboardswhen the site is running.
Purpose
Day 40's lesson explains the three pillars of observability — logs, metrics, and traces — and how they feed dashboards and alerts. This lab makes them concrete by building the whole pipeline from nothing but plain log lines: a tiny "app" emits structured JSON logs, you derive real metrics from them (total requests, error rate, and a p95 percentile), print a text dashboard, and reconstruct a trace of nested spans. By the end you have seen that there is no magic in any pillar — each is just events, counting, and arithmetic.
Learning objectives
- Emit structured JSON logs with a timestamp, level, event, and latency.
- Derive metrics from raw logs: a total (counter), an error rate, and a p95 latency computed as a real nearest-rank percentile.
- Print a small text dashboard of those metrics.
- Reconstruct a trace of nested spans from span start/end log lines.
- Run an automated test script and interpret its pass/fail output.
Prerequisites
- The Day 40 lesson (read it first — it explains every concept this lab builds).
- A terminal: Terminal.app (macOS), any terminal (Linux), or WSL (Windows).
bashandpython3(both preinstalled on macOS and Linux; see Required software).
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon).
- Linux — fully supported (any distribution with bash and python3).
- Windows — run the scripts unmodified inside WSL (Windows Subsystem for Linux).
Hardware requirements
Any computer made in roughly the last 15 years. The lab only generates a few kilobytes of synthetic logs in a temporary file; it needs no minimum RAM, disk, or GPU.
Required software
bash(3.2 or newer — preinstalled on macOS and Linux).python3(used for the percentile and JSON span parsing — preinstalled on macOS and nearly every Linux distribution; check withpython3 --version).- Standard text utilities:
grep,sed,awk,head,seq,mktemp— all part of the base system.
Free and open-source options
Everything in this lab is free and open source: bash, python3, and every utility used ship with your OS or are open-source. No account, API key, network access, or purchase is needed. The lesson's tool survey (Prometheus, Grafana, OpenTelemetry, the ELK and Loki stacks) is likewise free and open source — this lab teaches the ideas underneath those tools by hand.
Installation
None. Clone the repository (or copy this directory) and you are ready:
cd labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards
File structure
day-040-observability-logs-metrics-traces-and-dashboards/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── observe.sh ← YOUR working file (4 exercises)
│ └── observability-worksheet.md ← worksheet for the practice assignment
├── examples/
│ └── observe.sh ← completed reference pipeline
├── tests/
│ └── run_tests.sh ← automated checks
├── expected-output/
│ ├── sample-run.txt ← real captured run (macOS, Apple Silicon)
│ ├── test-run.txt ← real captured test result
│ └── FIELDS.md ← the fixed metrics a correct run produces
├── requirements/
│ └── README.md ← dependency statement (bash + python3)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the finished pipeline first
bash examples/observe.sh
## 2. Your task: complete the four exercises in the starter, then run it
bash starter/observe.sh
## 3. Check your work
bash tests/run_tests.sh
What the commands do
bash examples/observe.sh— runs the complete reference: an "app" writes 50 structured JSON log lines to a temporary file (6 of them atERRORlevel), then the script counts the requests (a counter), counts the errors and computes the error rate, computes the p95 and p50 latency as real nearest-rank percentiles withpython3, prints a text dashboard, and reconstructs a trace of nested spans from span start/end lines. The temporary log file is removed automatically on exit.bash starter/observe.sh— the same pipeline with four values left asFILL_ME; each exercise comment names the exact command to use. Edit the file and replace eachFILL_MEwith the real command.bash tests/run_tests.sh— runs the reference strictly (structured logs emitted, error count is 6, error rate is 12.00%, p95 is a number, trace reconstructed) and checks the starter's structure until you finish it.
Expected output
See expected-output/sample-run.txt — a real
captured run. Because the workload is deterministic, a correct run produces the
same metrics on any platform:
=== Observability Dashboard ===
Total requests: 50
Errors: 6
Error rate: 12.00%
p95 latency (ms): 2900
p50 latency (ms): 180
=== Trace: request trace-7f3a9c ===
span total_request 812 ms
span validate_input 12 ms
span db_query 540 ms
span call_service 248 ms
=== End of dashboard ===
expected-output/FIELDS.md explains why each
number is what it is.
Validation steps
- Run
bash examples/observe.sh— it prints the dashboard and trace above. - Complete the four exercises in
starter/observe.shand run it; confirm noFILL_MEremains and the dashboard shows an error rate of12.00%and a numeric p95. - Confirm the p95 latency is at or above the p50 (the slow tail is higher than the median).
- Run the tests (next section) — all checks must pass.
Tests
bash tests/run_tests.sh
Expected final line: 22 checks, 0 failure(s). The command exits 0 on success
and non-zero on any failure, so it can run in CI. It reaches no network.
Cleanup
Nothing to clean up: the scripts write only to a temporary file that is deleted
automatically when they exit, and they change no settings and reach no network.
To reset your work, restore the starter from git:
git checkout -- starter/observe.sh.
Troubleshooting
See troubleshooting.md for the full list (missing
python3, empty p95, error rate stuck at zero, permission messages, octal
arithmetic, and WSL notes).
Security notes
See security.md. Short version: the scripts run no network calls, need no elevated privileges, and log only synthetic data. The lesson the lab reinforces is that real logs can leak secrets and personal data — so you never log credentials, tokens, or full request bodies without scrubbing them.
Extension exercises
- Add the p99 latency to your dashboard alongside the p95, and note how far apart they are — a large gap means a few requests are dramatically slower than the rest.
- Add a
regionfield (such as"us"or"eu") to each structured log line, then compute the error rate per region — a question the dashboard was not built to answer, which structured logging lets you ask after the fact. - Change the alerting threshold: decide the error rate at which you would page an on-call engineer, and write one sentence justifying it as a user-facing symptom rather than an internal cause.
Navigation
- Previous day: Day 39 — Data Storage: Files, Databases, Object Storage, and Caches (
labs/sections/computing-foundations/day-039-data-storage-files-databases-object-storage/). - Next day: Day 41 — Thinking in Automation: Scripts, Hooks, and Pipelines (
labs/sections/computing-foundations/day-041-thinking-in-automation-scripts-hooks-and/, to be written).
Expected output
FIELDS.md
# Expected output — Day 040 lab
`sample-run.txt` is a real captured run of `examples/observe.sh` on the
authoring machine (macOS, Apple Silicon, 2026-07-12). `test-run.txt` is the
captured result of `tests/run_tests.sh`.
The workload is **deterministic**, so a correct run on any platform (macOS or
Linux, bash + python3) produces the same metrics:
| Field | Value | Why |
| --- | --- | --- |
| Total requests | `50` | The app emits 50 `request_complete` log lines |
| Errors | `6` | Every 8th request fails: requests 8, 16, 24, 32, 40, 48 |
| Error rate | `12.00%` | 6 ÷ 50 = 0.12 |
| p95 latency (ms) | `2900` | Nearest-rank p95 over the 50 latencies falls in the slow error tail |
| p50 latency (ms) | `180` | The median of the healthy 90–259 ms requests |
| Trace spans | `total_request` (812 ms) with 3 nested children | Durations reconstructed from span start/end lines |
The only line that legitimately varies between platforms is nothing in the
metrics — the numbers above are fixed. The sample structured log lines show
the exact JSON shape every entry uses: `ts`, `level`, `event`, `request_id`,
and `latency_ms`.
A completed `starter/observe.sh` (all four `FILL_ME` exercises finished)
produces the same Dashboard and Trace blocks as the reference.
sample-run.txt
=== Sample structured logs (2 healthy, 1 error) ===
{"ts":"2026-07-12T14:00:01Z","level":"INFO","event":"request_complete","request_id":"req-0001","latency_ms":130}
{"ts":"2026-07-12T14:00:02Z","level":"INFO","event":"request_complete","request_id":"req-0002","latency_ms":170}
{"ts":"2026-07-12T14:00:08Z","level":"ERROR","event":"request_complete","request_id":"req-0008","latency_ms":2900}
=== Observability Dashboard ===
Total requests: 50
Errors: 6
Error rate: 12.00%
p95 latency (ms): 2900
p50 latency (ms): 180
=== Trace: request trace-7f3a9c ===
span total_request 812 ms
span validate_input 12 ms
span db_query 540 ms
span call_service 248 ms
=== End of dashboard ===
test-run.txt
Testing <repo>/labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards/examples/observe.sh ...
ok: script exits successfully
ok: emits structured JSON request logs with latency_ms
ok: emits at least one ERROR-level log line
ok: dashboard prints 'Total requests:'
ok: dashboard prints 'Errors:'
ok: dashboard prints 'Error rate:'
ok: dashboard prints 'p95 latency (ms):'
ok: dashboard prints 'p50 latency (ms):'
ok: trace reconstructs the total_request span
ok: total requests is 50
ok: error count matches known injected errors (6)
ok: error rate is 12.00%
ok: p95 latency is a number
Note: starter/observe.sh still has unfinished exercises (FILL_ME) — testing structure only.
Testing <repo>/labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards/starter/observe.sh ...
ok: script exits successfully
ok: emits structured JSON request logs with latency_ms
ok: emits at least one ERROR-level log line
ok: dashboard prints 'Total requests:'
ok: dashboard prints 'Errors:'
ok: dashboard prints 'Error rate:'
ok: dashboard prints 'p95 latency (ms):'
ok: dashboard prints 'p50 latency (ms):'
ok: trace reconstructs the total_request span
22 checks, 0 failure(s).
Source files
examples/observe.sh (5131 bytes)
#!/usr/bin/env bash
# Day 040 lab — completed reference: a mini observability pipeline.
#
# It builds all three pillars from nothing but plain log lines:
# 1. an "app" that does work and emits STRUCTURED JSON logs (with an ERROR level)
# 2. METRICS derived from those logs (total, errors, error rate, p95/p50 latency)
# 3. a tiny text DASHBOARD that prints the metrics
# 4. a TRACE: nested span start/end lines for one request, timing reconstructed
#
# Offline, no network, no API key. Uses only bash, grep/sed/awk, and python3
# (preinstalled on macOS and Linux) for the percentile computation.
# The temporary log file is removed automatically on exit.
set -euo pipefail
# --- temp log file, cleaned up on exit -------------------------------------
logfile="$(mktemp -t observe_log.XXXXXX)"
cleanup() { rm -f "${logfile}"; }
trap cleanup EXIT
# --- 1. the "app": do work and emit structured JSON logs -------------------
# 50 requests. Every 8th request fails (level ERROR) and is slow, so the error
# count is deterministic (6 errors). Latencies are deterministic so every run
# of this reference produces the same metrics.
emit_log() {
# args: level event latency_ms request_id
local level="$1" event="$2" latency="$3" rid="$4"
local sec ts
sec=$(( 10#${rid##*-} % 60 ))
ts="$(printf '2026-07-12T14:00:%02dZ' "${sec}")"
printf '{"ts":"%s","level":"%s","event":"%s","request_id":"%s","latency_ms":%d}\n' \
"${ts}" "${level}" "${event}" "${rid}" "${latency}" >> "${logfile}"
}
run_app() {
local i level latency rid
for i in $(seq 1 50); do
rid="$(printf 'req-%04d' "${i}")"
if (( i % 8 == 0 )); then
level="ERROR"
latency=$(( 2000 + (i % 5) * 300 )) # slow, failing requests
else
level="INFO"
latency=$(( 90 + (i * 40) % 170 )) # healthy requests: 90-259 ms
fi
emit_log "${level}" "request_complete" "${latency}" "${rid}"
done
}
# --- 4. a trace: nested span start/end lines for ONE request ----------------
emit_span() {
# args: phase(start|end) span at_ms
printf '{"level":"INFO","event":"span_%s","trace_id":"trace-7f3a9c","span":"%s","at_ms":%d}\n' \
"$1" "$2" "$3" >> "${logfile}"
}
run_traced_request() {
emit_span start total_request 0
emit_span start validate_input 0
emit_span end validate_input 12
emit_span start db_query 12
emit_span end db_query 552
emit_span start call_service 552
emit_span end call_service 800
emit_span end total_request 812
}
# --- 2. derive METRICS from the logs (the three pillars, by hand) ----------
compute_metrics() {
local total errors rate
total="$(grep -c '"event":"request_complete"' "${logfile}")"
errors="$(grep -c '"level":"ERROR"' "${logfile}")"
# error rate as a percentage, two decimals, computed with awk
rate="$(awk -v e="${errors}" -v t="${total}" 'BEGIN{ printf "%.2f", (t>0)? (e/t)*100 : 0 }')"
# p95 and p50 latency: extract the numeric latency_ms of every request, then
# compute real nearest-rank percentiles in python3.
local pcts p95 p50
pcts="$(grep '"event":"request_complete"' "${logfile}" \
| sed -n 's/.*"latency_ms":\([0-9]*\).*/\1/p' \
| python3 -c '
import sys, math
vals = sorted(int(x) for x in sys.stdin.read().split())
n = len(vals)
def pct(p):
if n == 0: return 0
return vals[math.ceil(p/100*n) - 1]
print(pct(95), pct(50))
')"
p95="${pcts%% *}"
p50="${pcts##* }"
# --- 3. the DASHBOARD ----------------------------------------------------
echo "=== Observability Dashboard ==="
printf 'Total requests: %s\n' "${total}"
printf 'Errors: %s\n' "${errors}"
printf 'Error rate: %s%%\n' "${rate}"
printf 'p95 latency (ms): %s\n' "${p95}"
printf 'p50 latency (ms): %s\n' "${p50}"
}
# --- reconstruct the TRACE timing from the span start/end lines ------------
print_trace() {
echo "=== Trace: request trace-7f3a9c ==="
# For each span, pair its start and end at_ms and print the duration, indented
# so children sit under the parent. python3 parses the JSON span lines (this
# runs identically on macOS and Linux, unlike GNU-only awk extensions).
grep '"event":"span_' "${logfile}" | python3 -c '
import sys, json
start, dur, order = {}, {}, []
for line in sys.stdin:
line = line.strip()
if not line:
continue
r = json.loads(line)
name = r["span"]
if r["event"] == "span_start":
start[name] = r["at_ms"]
order.append(name)
else:
dur[name] = r["at_ms"] - start[name]
for name in order:
indent = " " if name == "total_request" else " "
print("%sspan %-16s %4d ms" % (indent, name, dur[name]))
'
}
# --- run the pipeline ------------------------------------------------------
run_app
run_traced_request
# Show a few raw structured log lines so the format is visible, including one
# ERROR line so you can see how a failure is recorded.
echo "=== Sample structured logs (2 healthy, 1 error) ==="
grep '"level":"INFO"' "${logfile}" | grep '"event":"request_complete"' | head -n 2
grep '"level":"ERROR"' "${logfile}" | head -n 1
echo
compute_metrics
print_trace
echo "=== End of dashboard ==="
metadata.yml (570 bytes)
lesson_id: D040
day: 40
kind: data-processing
languages: [bash]
setup_commands:
- cd labs/sections/computing-foundations/day-040-observability-logs-metrics-traces-and-dashboards
run_commands:
- bash examples/observe.sh
- bash starter/observe.sh
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/observe.sh # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), bash tests/run_tests.sh → 22 checks, 0 failure(s)'
requirements/README.md (859 bytes)
# Dependencies — Day 040 lab
**Two tools, both preinstalled on macOS and Linux:**
- `bash` ≥ 3.2 — the shell that runs the pipeline (preinstalled on macOS and
every mainstream Linux distribution).
- `python3` — used only for the percentile computation (the p95/p50) and to
parse the JSON span lines when reconstructing the trace. It ships with macOS
and nearly every Linux distribution. Check with `python3 --version`.
The lab also uses the standard text utilities `grep`, `sed`, `awk`, `head`,
`seq`, and `mktemp`, all part of the base system on macOS and Linux.
There is deliberately no `requirements.txt` or `package.json`: the lab installs
nothing and reaches no network. If `python3` is somehow missing, install it
from your package manager (for example `sudo apt install python3` on
Debian/Ubuntu); everything else is already present.
starter/observability-worksheet.md (1353 bytes)
# Observability worksheet — Day 040
Fill this in from a real run of your completed `starter/observe.sh` (or the
reference `examples/observe.sh`). Keep it — it is the template you will reuse
when you instrument a real service later.
## The metrics from my run
| Metric | My value |
| --- | --- |
| Total requests | |
| Error count | |
| Error rate (%) | |
| p95 latency (ms) | |
| p50 latency (ms) | |
## The one thing a dashboard should alert on
Write the single alert you would set for this service. State it as a **symptom
the user feels**, not an internal cause, and give a concrete threshold and a
duration.
- Alert when: ______________________________________________
- Because (the user impact): ________________________________
Example shape: "Page when the error rate stays above 5% for 5 minutes, because
that means more than one user in twenty is seeing failures."
## If this service suddenly felt slow, which pillar would I check first?
In three or four sentences, name which of the three pillars (metrics, traces,
or logs) you would consult first, and what each would tell you next.
______________________________________________________________
______________________________________________________________
______________________________________________________________
## One thing that surprised me
One or two sentences.
starter/observe.sh (5516 bytes)
#!/usr/bin/env bash
# Day 040 lab — STARTER: build a mini observability pipeline.
#
# The "app" that emits structured JSON logs and the trace reconstruction are
# provided and working. YOUR JOB is the four numbered exercises below, which
# build the three pillars from those logs. Each exercise names the exact
# command to use. Replace every placeholder, then run: bash starter/observe.sh
# Check the finished reference any time: bash examples/observe.sh
set -euo pipefail
# --- temp log file, cleaned up on exit (provided) --------------------------
logfile="$(mktemp -t observe_log.XXXXXX)"
cleanup() { rm -f "${logfile}"; }
trap cleanup EXIT
# --- the "app": emits structured JSON logs, 6 of 50 requests fail (provided)
emit_log() {
local level="$1" event="$2" latency="$3" rid="$4"
local sec ts
sec=$(( 10#${rid##*-} % 60 ))
ts="$(printf '2026-07-12T14:00:%02dZ' "${sec}")"
printf '{"ts":"%s","level":"%s","event":"%s","request_id":"%s","latency_ms":%d}\n' \
"${ts}" "${level}" "${event}" "${rid}" "${latency}" >> "${logfile}"
}
run_app() {
local i level latency rid
for i in $(seq 1 50); do
rid="$(printf 'req-%04d' "${i}")"
if (( i % 8 == 0 )); then
level="ERROR"; latency=$(( 2000 + (i % 5) * 300 ))
else
level="INFO"; latency=$(( 90 + (i * 40) % 170 ))
fi
emit_log "${level}" "request_complete" "${latency}" "${rid}"
done
}
# --- the trace: nested span start/end lines for one request (provided) ------
emit_span() {
printf '{"level":"INFO","event":"span_%s","trace_id":"trace-7f3a9c","span":"%s","at_ms":%d}\n' \
"$1" "$2" "$3" >> "${logfile}"
}
run_traced_request() {
emit_span start total_request 0
emit_span start validate_input 0; emit_span end validate_input 12
emit_span start db_query 12; emit_span end db_query 552
emit_span start call_service 552; emit_span end call_service 800
emit_span end total_request 812
}
print_trace() {
echo "=== Trace: request trace-7f3a9c ==="
grep '"event":"span_' "${logfile}" | python3 -c '
import sys, json
start, dur, order = {}, {}, []
for line in sys.stdin:
line = line.strip()
if not line: continue
r = json.loads(line)
name = r["span"]
if r["event"] == "span_start":
start[name] = r["at_ms"]; order.append(name)
else:
dur[name] = r["at_ms"] - start[name]
for name in order:
indent = " " if name == "total_request" else " "
print("%sspan %-16s %4d ms" % (indent, name, dur[name]))
'
}
# ===========================================================================
# YOUR WORK STARTS HERE. Run the app first so the log file exists.
# ===========================================================================
run_app
run_traced_request
# Sample of the structured logs the app just wrote (provided, so you can see
# the format you are working with — two healthy lines and one error line).
echo "=== Sample structured logs (2 healthy, 1 error) ==="
grep '"level":"INFO"' "${logfile}" | grep '"event":"request_complete"' | head -n 2
grep '"level":"ERROR"' "${logfile}" | head -n 1
echo
# ---------------------------------------------------------------------------
# EXERCISE 1: emit ONE structured log line to prove you can produce the format.
# Replace the echo placeholder with a printf that prints a single JSON object with the keys
# ts, level, event, and latency_ms (any sample values are fine). Example shape:
# printf '{"ts":"2026-07-12T14:00:00Z","level":"INFO","event":"demo","latency_ms":123}\n'
echo "=== Your structured log line (Exercise 1) ==="
echo 'FILL_ME' # <- replace this echo with your printf that prints one JSON log line
echo
# total requests = number of request_complete lines (provided, uses grep -c)
total="$(grep -c '"event":"request_complete"' "${logfile}")"
# ---------------------------------------------------------------------------
# EXERCISE 2: count the ERROR requests.
# Set `errors` to the number of log lines whose level is ERROR, using:
# grep -c '"level":"ERROR"' "${logfile}"
errors="FILL_ME"
# ---------------------------------------------------------------------------
# EXERCISE 3: compute the error RATE as a percentage with two decimals.
# Use awk with the two numbers you already have. Template:
# awk -v e="${errors}" -v t="${total}" 'BEGIN{ printf "%.2f", (t>0)?(e/t)*100:0 }'
rate="FILL_ME"
# ---------------------------------------------------------------------------
# EXERCISE 4: compute the p95 latency (a real nearest-rank percentile).
# Extract the latency_ms of every request and pipe it to python3. Template:
# grep '"event":"request_complete"' "${logfile}" \
# | sed -n 's/.*"latency_ms":\([0-9]*\).*/\1/p' \
# | python3 -c 'import sys,math; v=sorted(int(x) for x in sys.stdin.read().split()); print(v[math.ceil(0.95*len(v))-1])'
p95="FILL_ME"
# p50 (median) latency is provided as a worked example of the same technique.
p50="$(grep '"event":"request_complete"' "${logfile}" \
| sed -n 's/.*"latency_ms":\([0-9]*\).*/\1/p' \
| python3 -c 'import sys,math; v=sorted(int(x) for x in sys.stdin.read().split()); print(v[math.ceil(0.50*len(v))-1])')"
# --- the DASHBOARD (provided) ----------------------------------------------
echo "=== Observability Dashboard ==="
printf 'Total requests: %s\n' "${total}"
printf 'Errors: %s\n' "${errors}"
printf 'Error rate: %s%%\n' "${rate}"
printf 'p95 latency (ms): %s\n' "${p95}"
printf 'p50 latency (ms): %s\n' "${p50}"
print_trace
echo "=== End of dashboard ==="
tests/run_tests.sh (3576 bytes)
#!/usr/bin/env bash
# Tests for the Day 040 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# Verifies the observability pipeline: structured JSON logs are emitted, the
# error count and rate match the known injected errors (6 of 50 = 12.00%), and
# a numeric p95 latency is computed. Runs fully offline; exits 0 on success.
set -u
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
run_pipeline_checks() {
local script="$1" strict="$2" output
echo "Testing ${script} ..."
if ! output="$(bash "${script}" 2>&1)"; then
check "script exits successfully" "no"
echo "${output}" | sed 's/^/ /'
return
fi
check "script exits successfully" "yes"
# Pillar 1: structured JSON logs are emitted (a line with the required keys).
if echo "${output}" | grep -Eq '\{"ts":".*","level":".*","event":"request_complete".*"latency_ms":[0-9]+\}'; then
check "emits structured JSON request logs with latency_ms" "yes"
else
check "emits structured JSON request logs with latency_ms" "no"
fi
# At least one ERROR-level line exists in the emitted logs.
echo "${output}" | grep -q '"level":"ERROR"' && \
check "emits at least one ERROR-level log line" "yes" || \
check "emits at least one ERROR-level log line" "no"
# Dashboard shape: all five metric lines present.
for field in "Total requests:" "Errors:" "Error rate:" "p95 latency (ms):" "p50 latency (ms):"; do
echo "${output}" | grep -q "^${field}" && check "dashboard prints '${field}'" "yes" || check "dashboard prints '${field}'" "no"
done
# Trace: nested spans reconstructed.
echo "${output}" | grep -q "span total_request" && \
check "trace reconstructs the total_request span" "yes" || \
check "trace reconstructs the total_request span" "no"
if [ "${strict}" = "strict" ]; then
# Pillar 2: metrics match the known injected errors (6 of 50).
total="$(echo "${output}" | sed -n 's/^Total requests:[[:space:]]*//p')"
errors="$(echo "${output}" | sed -n 's/^Errors:[[:space:]]*//p')"
rate="$(echo "${output}" | sed -n 's/^Error rate:[[:space:]]*//p')"
p95="$(echo "${output}" | sed -n 's/^p95 latency (ms):[[:space:]]*//p')"
[ "${total}" = "50" ] && check "total requests is 50" "yes" || check "total requests is 50 (got '${total}')" "no"
[ "${errors}" = "6" ] && check "error count matches known injected errors (6)" "yes" || check "error count is 6 (got '${errors}')" "no"
[ "${rate}" = "12.00%" ] && check "error rate is 12.00%" "yes" || check "error rate is 12.00% (got '${rate}')" "no"
case "${p95}" in
'' | *[!0-9]*) check "p95 latency is a number (got '${p95}')" "no" ;;
*) check "p95 latency is a number" "yes" ;;
esac
fi
}
# The completed reference must pass every strict check.
run_pipeline_checks "${lab_dir}/examples/observe.sh" strict
# The starter ships with FILL_ME placeholders; once the learner has replaced
# them all, hold their script to the same strict standard.
if grep -q 'FILL_ME' "${lab_dir}/starter/observe.sh"; then
echo "Note: starter/observe.sh still has unfinished exercises (FILL_ME) — testing structure only."
run_pipeline_checks "${lab_dir}/starter/observe.sh" lenient
else
run_pipeline_checks "${lab_dir}/starter/observe.sh" strict
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 040 lab
python3: command not found
The percentile step and the trace reconstruction use python3. It ships with
macOS and nearly every Linux distribution; if it is missing, install it from
your package manager (for example sudo apt install python3 on Debian/Ubuntu),
then re-run. Check with python3 --version.
The p95 line prints nothing or an error
You are probably feeding whole log lines into python instead of just the
numeric latency. Extract the number first with the provided sed step:
grep '"event":"request_complete"' "${logfile}" | sed -n 's/.*"latency_ms":\([0-9]*\).*/\1/p'
That should print one integer per line. If it prints nothing, your sed
pattern does not match the log format — copy it exactly, including the quotes.
The error rate is always 0.00% (or FILL_ME)
Your error count is matching nothing. Confirm you are searching for the exact
string "level":"ERROR" — with the quotes and capital ERROR — as it appears in
the log file. In the starter, this is Exercise 2; you must replace the
errors="FILL_ME" line with the real grep -c command.
Permission denied when running the script
Run it through bash explicitly rather than executing the file directly:
bash examples/observe.sh
If you prefer ./examples/observe.sh, first make it executable with
chmod +x examples/observe.sh.
value too great for base from bash
This happens if a request-id number is treated as octal. The scripts already
guard against it with 10# (forcing base-10 arithmetic). If you copied the
emit_log function and removed that, restore the 10#${rid##*-} form.
The trace durations look wrong or the spans are out of order
The trace is reconstructed by pairing each span_start with its span_end by
name. If you edited run_traced_request, make sure every span that starts also
ends, and that total_request starts first and ends last so it is the parent.
Tests fail with FILL_ME still present
That is expected until you finish the starter: the tests run the reference
strictly and check the starter's structure only while FILL_ME remains. Once
you replace all four FILL_ME markers, the tests hold your starter to the full
strict standard.
Windows
Use WSL (wsl --install, then open Ubuntu) and run the Linux commands
unmodified. The scripts are plain bash + python3 and run identically under WSL.
Security notes
Security notes — Day 040 lab
-
What the scripts do: generate synthetic log lines in a temporary file, compute metrics from them, and print a dashboard and a trace. They make no network connections, need no elevated privileges, and write only to a temporary file that is removed automatically on exit (via a
trap). -
Logs can leak secrets and personal data — the central lesson. In the real world, logs are one of the most common places credentials and personal information leak: a careless line that records a full request can capture a password, an API key, a session token, or a user's personal details in plain text, and that log may then be shipped to a third-party service and retained for a long time. Never log credentials, tokens, or full request bodies without scrubbing them first. Treat logs as sensitive data.
-
This lab logs only synthetic data. Every value the scripts emit is made up — fixed timestamps, fake request ids, and computed latencies. Nothing about your machine, your accounts, or any real person is collected or written. This is deliberate: it lets you practice the mechanics of logging without ever handling sensitive data.
-
Privacy in practice: because real logs and traces record real user activity, they often fall under privacy regulations and data-retention rules. Responsible instrumentation logs identifiers rather than raw personal data where possible, sets retention limits, and can delete a user's records on request. Keep this in mind the first time you point logging at a real service.
-
Reading before running: both scripts are short and commented — read them first. Running unread shell scripts is a common way developers get compromised; every script in this course is small enough to read before you run it.