Pair START and END lines to find slow and stuck requests
Problem statement
A service logs a START line when a request begins and an END line when it finishes. Requests run concurrently, so lines from different requests are interleaved. Each line has a timestamp HH:MM:SS.mmm, the event, and key=value fields including req, the request ID.
Report:
- The 3 slowest completed requests: ID, path and duration in milliseconds, slowest first.
- Requests that started but never finished (stuck, or the process died).
ENDlines whoseSTARTis not in the file (it was in the previous log file).
trace.log
10:00:00.100 START req=a1 path=/api/users10:00:00.150 START req=b2 path=/api/orders10:00:00.180 END req=a1 status=20010:00:00.400 START req=c3 path=/api/login10:00:01.020 END req=b2 status=50010:00:01.100 END req=zz status=20010:00:01.300 START req=d4 path=/api/users10:00:01.450 END req=d4 status=200Examples
Example 1
Input: python solution.py trace.log
Output: b2 /api/orders 870 ms
d4 /api/users 150 ms
a1 /api/users 80 ms
never finished: c3
end without start: zz
Explanation: b2 starts at 00.150 and ends at 01.020: 870 ms. c3 has no END. zz has an END but no START.
Hints
Approach
Optimal
This is a matching problem with a hash map.
- Convert the timestamp to milliseconds since midnight. Pad the fraction to 3 digits so
.1means 100 ms, not 1 ms. - On
START, storeopen_reqs[req] = (ms, path). - On
END, pop the ID. If it is not there, record an orphan END. Otherwise compute the duration and push it into a min-heap that never grows pastk:heappushpopadds the new item and removes the smallest in one step. - At the end, the heap holds the
kslowest requests andopen_reqsholds the unfinished ones.
Memory is the number of requests in flight at once plus k, not the total number of requests. That is what makes this workable on a day of traffic.
O(n log k)Space O(open + k)import heapqimport sys def to_ms(ts): """'10:00:01.020' -> milliseconds since midnight.""" hms, _, frac = ts.partition(".") h, m, s = (int(x) for x in hms.split(":")) return ((h * 60 + m) * 60 + s) * 1000 + int(frac.ljust(3, "0")[:3]) def pair_requests(lines, k=3): open_reqs = {} # req id -> (start_ms, path); only in-flight requests live here slowest = [] # min-heap of (duration, req, path), never larger than k orphans_end = [] bad = 0 for line in lines: parts = line.split() if len(parts) < 3: if parts: bad += 1 continue fields = dict(p.split("=", 1) for p in parts[2:] if "=" in p) req = fields.get("req") try: ms = to_ms(parts[0]) except ValueError: bad += 1 continue if req is None or parts[1] not in ("START", "END"): bad += 1 continue if parts[1] == "START": open_reqs[req] = (ms, fields.get("path", "?")) continue if req not in open_reqs: orphans_end.append(req) continue start, path = open_reqs.pop(req) item = (ms - start, req, path) if len(slowest) < k: heapq.heappush(slowest, item) else: heapq.heappushpop(slowest, item) # drops the fastest of the k+1 return sorted(slowest, reverse=True), list(open_reqs), orphans_end, bad def main(path): with open(path, encoding="utf-8", errors="replace") as f: slowest, unfinished, orphans, bad = pair_requests(f) for dur, req, p in slowest: print(f"{req} {p} {dur} ms") print("never finished:", ", ".join(unfinished) or "-") print("end without start:", ", ".join(orphans) or "-") if bad: print(f"skipped {bad} malformed line(s)", file=sys.stderr) if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else "trace.log")Follow-up questions
- Requests stuck for more than 30 seconds should be reported while the log is still being read, not only at the end.
- The START and END for one request are in different files from different hosts. How do you join them?
- Report p95 duration per path instead of the top 3 overall.
Frequently asked questions
This version overwrites the first START, so the first attempt disappears. Depending on the system, a repeated ID could be a retry, a bug, or IDs being reused. In an interview, say which you assume. A safer option is to report the earlier START as unfinished before replacing it.
Correlating events by request ID is how you debug distributed systems without a tracing product: find the slow requests and find the ones that never returned. It also tests whether you notice that memory should depend on concurrency, not on total volume.
With time-of-day stamps, an END at 00:00:00.100 looks earlier than a START at 23:59:59.900. If the duration comes out negative, add 24 hours, or better, log full ISO-8601 timestamps so the problem cannot arise.