Lines Between Two Times
Problem statement
Print only the log lines between two times, including the multi-line stack trace that belongs to an error in that window. When someone says "it broke between 10:05 and 10:10", this is how you cut exactly that slice out of a large log, without missing lines and without running to the end of the file.
app.log
2026-10-01T10:00:12Z INFO request ok2026-10-01T10:04:59Z INFO request ok2026-10-01T10:05:00Z WARN queue growing2026-10-01T10:07:31Z ERROR worker crashed at Worker.run(worker.py:42) at Pool.start(pool.py:10)2026-10-01T10:10:00Z INFO worker restarted2026-10-01T10:10:01Z INFO request ok2026-10-01T10:15:00Z INFO request ok- Show the
sedrange trap: a range that starts at a time with no log line prints nothing. - Print every line from
10:05:00to10:10:00, inclusive, using a text comparison of the timestamps. - Keep the indented stack trace lines with the error they belong to.
Expected output:
== sed range starting at a time with no log line ==0== lines from 10:05:00 to 10:10:00, stack trace included ==2026-10-01T10:05:00Z WARN queue growing2026-10-01T10:07:31Z ERROR worker crashed at Worker.run(worker.py:42) at Pool.start(pool.py:10)2026-10-01T10:10:00Z INFO worker restartedHints
$1 >= "2026-10-01T10:05:00" && $1 <= "2026-10-01T10:10:00Z" is a real time comparison.Approach
Optimal: awk string compare with a state flag
Covers: why ISO timestamps compare as text, sed -n '/a/,/b/p' and its traps, awk >= and <= on strings, -v variables, state that carries across lines, multi-line entries.
The sed range looks easy, but it is fragile. sed -n '/10:05/,/10:10/p' prints from the first line matching 10:05 to the next line matching 10:10. Two traps:
- If no line matches the start, for example nothing was logged at
10:06, the range never starts and you get nothing. - If no line matches the end, the range runs to the end of the file.
Patterns also match anywhere on the line, so 10:05 could match an ID or a duration. Timestamps are values, so compare them as values.
ISO timestamps compare correctly as text. In 2026-10-01T10:07:31Z every part has a fixed width and the biggest unit comes first. So the normal alphabetical comparison gives time order: "2026-10-01T10:05:00" < "2026-10-01T10:07:31" is true. awk compares two strings this way when you use <, >= and so on. This works for any time, whether or not a line was logged at that exact moment.
| Timestamp | >= from |
<= to |
In the window? |
|---|---|---|---|
...T10:04:59Z |
no | yes | no |
...T10:05:00Z |
yes | yes | yes |
...T10:10:00Z |
yes | yes | yes |
...T10:10:01Z |
yes | no | no |
The to value is written with the Z, so the line at exactly 10:10:00Z is included: "...T10:10:00Z" <= "...T10:10:00Z" is true.
Multi-line entries need state. A stack trace is one log entry spread over several lines, and only the first line has a time. If you test every line on its own, the at Worker.run lines are dropped. Instead, the program decides once per dated line and remembers the answer:
keep = 1"]):::purple K --> T1["at Worker.run
no date: keep stays 1"]:::green T1 --> T2["at Pool.start
printed too"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
$1 ~ /^20[0-9][0-9]-/ { keep = ($1 >= from && $1 <= to) } a dated line: decide againkeep print while the decision is yesUndated lines leave keep as it was, so they follow the line they belong to.
Walking through the code. The # Setup: lines only create the sample log, so skip past them.
- The
sedrange starting atT10:06prints nothing, because no line contains that text.wc -lshows 0. - The awk gets the window with
-v from=... -v to=..., so the times are easy to change. - It prints the five lines from
10:05:00to10:10:00, including both stack trace lines under the error.
Edge cases. This needs every dated line to use the same format and time zone; mixing Z and +05:30 breaks text comparison. For logs like Oct 1 10:05:17, compare $3 (the time) and filter the day separately, or convert to epoch seconds. On huge logs, awk still reads the whole file; add $1 > to { exit } when the log is in order, so it stops after the window.
# Setup: create the sample log (with a stack trace) in a fresh temporary folder
cd "$(mktemp -d)"
cat > app.log << 'LOG'
2026-10-01T10:00:12Z INFO request ok
2026-10-01T10:04:59Z INFO request ok
2026-10-01T10:05:00Z WARN queue growing
2026-10-01T10:07:31Z ERROR worker crashed
at Worker.run(worker.py:42)
at Pool.start(pool.py:10)
2026-10-01T10:10:00Z INFO worker restarted
2026-10-01T10:10:01Z INFO request ok
2026-10-01T10:15:00Z INFO request ok
LOG
echo "== sed range starting at a time with no log line =="
sed -n '/T10:06/,/T10:10/p' app.log | wc -l
echo "== lines from 10:05:00 to 10:10:00, stack trace included =="
awk -v from="2026-10-01T10:05:00Z" -v to="2026-10-01T10:10:00Z" '
$1 ~ /^20[0-9][0-9]-/ { keep = ($1 >= from && $1 <= to) } # dated line: decide
keep # print while inside
' app.logInterview follow-ups
Take the window from "the 5 minutes before the first ERROR" to "the first ERROR", automatically.
Find the first error's time with
t=$(awk '$2 == "ERROR" { print $1; exit }' app.log). Work out the start with GNU date:from=$(date -u -d "$(echo "$t" | sed 's/T/ /; s/Z//') 5 minutes ago" +%Y-%m-%dT%H:%M:%SZ). Then run the same awk with-v from="$from" -v to="$t". This "what happened just before it broke" slice is often the most useful part of a log during an incident.
Frequently asked questions
Those lines have no year and the month is a word, so text comparison of the whole date does not work across days. Within one day, compare the time field: awk '$3 >= "10:05:00" && $3 <= "10:10:00"', which works because HH:MM:SS has a fixed width. For ranges over several days, convert each date to epoch seconds, for example with gawk's mktime, or use journalctl --since "2026-10-01 10:05" --until "2026-10-01 10:10" if the logs are in the journal.
It works for some windows, but the pattern gets messy fast: 10:05 to 10:10 needs T10:0[5-9]|T10:10:00, and 09:55 to 10:10 needs three parts. It also drops stack trace lines, which have no time. A comparison states the window directly and handles any start and end. Use grep for quick looks, and the awk version when the window matters.
grep -B 2 -A 5 ERROR app.log prints 2 lines before and 5 after each match, which often catches the trace and what led up to it. -C 3 gives 3 on both sides. It is a good first step when you do not know the exact window yet: find the error with context, read its time, then cut the precise window with the awk on this page.