Bash and Linux

Lines Between Two Times

mediumLogs and troubleshooting

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

TEXT
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
  1. Show the sed range trap: a range that starts at a time with no log line prints nothing.
  2. Print every line from 10:05:00 to 10:10:00, inclusive, using a text comparison of the timestamps.
  3. Keep the indented stack trace lines with the error they belong to.

Expected output:

TEXT
== 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 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

Hints

Hint 1: ISO timestamps sort as text in time order, so in awk $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:

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart LR subgraph S1["start never matches"] direction TB A1["no line at 10:06"]:::red --> A2["prints nothing"]:::red end subgraph S2["end never matches"] direction TB B1["no line at the end time"]:::red --> B2["prints to end of file"]:::red end S1 ~~~ S2 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 style S1 fill:transparent,stroke:#dc2626,stroke-width:2px style S2 fill:transparent,stroke:#dc2626,stroke-width:2px
  • 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:

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart TB D["10:07:31 ERROR worker crashed"]:::red --> K(["dated line: in window
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
◈ DIAGRAM
$1 ~ /^20[0-9][0-9]-/ { keep = ($1 >= from && $1 <= to) } a dated line: decide again
keep print while the decision is yes

Undated 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.

  1. The sed range starting at T10:06 prints nothing, because no line contains that text. wc -l shows 0.
  2. The awk gets the window with -v from=... -v to=..., so the times are easy to change.
  3. It prints the five lines from 10:05:00 to 10: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.log

Interview 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.