Skip to content

Commit 560c080

Browse files
authored
Merge pull request #93 from LAA-Software-Engineering/fix/timeline-severity-ordering-65
fix: order timeline effects by severity before timestamp (#65)
2 parents bbea27f + 1d0e21d commit 560c080

2 files changed

Lines changed: 74 additions & 12 deletions

File tree

‎src/core/timeline/builder.py‎

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,6 @@ def _classify_trigger(message: str) -> str:
6161
return "trigger"
6262

6363

64-
def _effect_severity(cluster: ClusterData) -> int:
65-
"""Lower = more severe = sorts first among effects."""
66-
msg = (cluster.representative_message or "").lower()
67-
if any(kw in msg for kw in ("500", "error", "fail", "exception", "refused", "denied")):
68-
return 0
69-
if any(kw in msg for kw in ("latency", "slow", "timeout", "degraded")):
70-
return 1
71-
return 2
72-
73-
7464
def _classify_secondary(cluster: ClusterData, primary: Optional[ClusterData]) -> str:
7565
msg = (cluster.representative_message or "").lower()
7666
if re.search(r"queue|pending|backlog", msg):
@@ -164,11 +154,14 @@ def build_timeline(packet: EvidencePacket) -> list[TimelineEvent]:
164154
services=services,
165155
))
166156

167-
# Sort effects by: severity first, then timestamp
157+
# Sort effects by: severity first, then timestamp. The category rank
158+
# (error < effect < symptom) and the within-category description rank must
159+
# dominate so error effects sort ahead of symptoms regardless of when each
160+
# first appeared; timestamp only breaks ties among equally-severe events.
168161
post_events.sort(key=lambda e: (
169-
e.timestamp,
170162
EFFECT_SEVERITY.get(e.category, 1),
171163
_effect_severity_from_desc(e.description),
164+
e.timestamp,
172165
))
173166

174167
result = pre_events

‎tests/unit/test_timeline.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from datetime import datetime, timedelta, timezone
2+
3+
from src.core.clustering.clusterer import ClusterData
4+
from src.core.explain.evidence import EvidencePacket
5+
from src.core.timeline.builder import build_timeline
6+
7+
BASE = datetime(2026, 1, 1, 22, 0, 0, tzinfo=timezone.utc)
8+
9+
10+
def _cluster(message: str, offset_minutes: int, fingerprint: str = "") -> ClusterData:
11+
ts = BASE + timedelta(minutes=offset_minutes)
12+
return ClusterData(
13+
fingerprint=fingerprint or message,
14+
representative_message=message,
15+
count=10,
16+
services={"api": 10},
17+
levels={"error": 10},
18+
first_seen=ts,
19+
last_seen=ts,
20+
baseline_count=0,
21+
change_ratio=0.0,
22+
importance_score=1.0,
23+
)
24+
25+
26+
def _packet(primary: ClusterData, secondary: list[ClusterData]) -> EvidencePacket:
27+
return EvidencePacket(
28+
window_start=BASE,
29+
window_end=BASE + timedelta(hours=1),
30+
total_logs=100,
31+
primary_cluster=primary,
32+
secondary_clusters=secondary,
33+
trigger_candidates=[],
34+
evidence_items=[],
35+
services_affected=["api"],
36+
)
37+
38+
39+
class TestTimelineOrdering:
40+
def test_error_effect_sorts_before_earlier_symptom(self):
41+
"""Regression for #65: severity ranks before timestamp.
42+
43+
A 500/error effect must sort ahead of a queue-backlog symptom even
44+
when the symptom appeared first — the docstring promises "error
45+
effects before … symptoms last", not chronological order.
46+
"""
47+
primary = _cluster("primary 500 error", 0)
48+
symptom = _cluster("queue backlog growing", 1) # earlier
49+
effect = _cluster("checkout 500 error failed", 5) # later, more severe
50+
51+
timeline = build_timeline(_packet(primary, [symptom, effect]))
52+
categories = [e.category for e in timeline]
53+
54+
# primary error first, then the effect, then the symptom last
55+
assert categories == ["error", "effect", "symptom"]
56+
# the later-but-more-severe effect precedes the earlier symptom
57+
eff_idx = next(i for i, e in enumerate(timeline) if "checkout" in e.description)
58+
sym_idx = next(i for i, e in enumerate(timeline) if "backlog" in e.description)
59+
assert eff_idx < sym_idx
60+
61+
def test_timestamp_breaks_ties_within_category(self):
62+
"""Equally-severe effects still order by timestamp."""
63+
primary = _cluster("primary 500 error", 0)
64+
later = _cluster("checkout 500 error late", 9)
65+
earlier = _cluster("payment 500 error early", 3)
66+
67+
timeline = build_timeline(_packet(primary, [later, earlier]))
68+
effects = [e.description for e in timeline if e.category == "effect"]
69+
assert effects.index("payment 500 error early") < effects.index("checkout 500 error late")

0 commit comments

Comments
 (0)