sre-agent-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sre_agent/__init__.py +3 -0
- sre_agent/adapters.py +833 -0
- sre_agent/approvals.py +243 -0
- sre_agent/audit.py +225 -0
- sre_agent/backend_scores.py +184 -0
- sre_agent/backend_url.py +146 -0
- sre_agent/cli.py +732 -0
- sre_agent/config.py +22 -0
- sre_agent/evaluation.py +391 -0
- sre_agent/evaluation_page.py +209 -0
- sre_agent/hosted_demo.py +153 -0
- sre_agent/ingestion.py +423 -0
- sre_agent/injection.py +580 -0
- sre_agent/logging_config.py +83 -0
- sre_agent/memory.py +411 -0
- sre_agent/model_backend.py +551 -0
- sre_agent/model_proposals.py +287 -0
- sre_agent/models.py +278 -0
- sre_agent/policy.py +208 -0
- sre_agent/postmortems.py +296 -0
- sre_agent/reasoning.py +547 -0
- sre_agent/regression.py +287 -0
- sre_agent/runbooks.py +290 -0
- sre_agent/security_review.py +826 -0
- sre_agent/server.py +684 -0
- sre_agent/storage.py +266 -0
- sre_agent/tier3.py +440 -0
- sre_agent/tools.py +712 -0
- sre_agent_cli-0.2.0.dist-info/METADATA +364 -0
- sre_agent_cli-0.2.0.dist-info/RECORD +34 -0
- sre_agent_cli-0.2.0.dist-info/WHEEL +5 -0
- sre_agent_cli-0.2.0.dist-info/entry_points.txt +2 -0
- sre_agent_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- sre_agent_cli-0.2.0.dist-info/top_level.txt +1 -0
sre_agent/memory.py
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Memory: past incidents, found again; runbook stats, kept honestly.
|
|
2
|
+
|
|
3
|
+
In plain words, this is the agent's memory of its own past work.
|
|
4
|
+
When a new incident arrives, an on-call engineer asks "have we
|
|
5
|
+
seen this before, and what fixed it last time?" This module is
|
|
6
|
+
that question, in code. It has three parts:
|
|
7
|
+
|
|
8
|
+
- **Storage** (``IncidentMemory``). Past incidents are kept in
|
|
9
|
+
memory, behind a small interface — the same pattern the
|
|
10
|
+
incident store, the tools and the runbooks use. Records are
|
|
11
|
+
stored as copies and handed back as match summaries, so a
|
|
12
|
+
caller cannot rewrite history by editing what it was given.
|
|
13
|
+
Incidents themselves can now be stored in a database
|
|
14
|
+
(``storage.py``, Phase 7); feeding a server's own stored,
|
|
15
|
+
resolved incidents back in as its memory is not wired yet,
|
|
16
|
+
and nothing here claims it is.
|
|
17
|
+
|
|
18
|
+
- **Retrieval** (``find_similar``). A new incident is scored
|
|
19
|
+
against each past one with fixed, explainable points: the
|
|
20
|
+
same service counts most, then the same namespace, then the
|
|
21
|
+
same environment, then words shared between the two records'
|
|
22
|
+
titles, diagnoses and recorded possible causes (a few points
|
|
23
|
+
at most, so wording can never outweigh the target). Every
|
|
24
|
+
match lists the reasons for its score, in plain words. There
|
|
25
|
+
is no model and no embedding here, on purpose — the same
|
|
26
|
+
choice the reasoning loop made — so every ranking can be
|
|
27
|
+
checked exactly in a test. A past record of the query incident
|
|
28
|
+
itself (same id) is never returned as its own match.
|
|
29
|
+
|
|
30
|
+
- **Runbook statistics and suggestions** (``runbook_stats``,
|
|
31
|
+
``suggest_runbooks``). Every runbook run leaves an action
|
|
32
|
+
record on its incident (``runbooks.py`` builds it), so the
|
|
33
|
+
stats are counted from those records, never from a separate
|
|
34
|
+
tally that could drift. A run counts as "ended resolved" when
|
|
35
|
+
its incident's final status is resolved, and as "not resolved"
|
|
36
|
+
otherwise. That wording is deliberate, and it is this pass's
|
|
37
|
+
one design lesson: the record shows the incident *ended*
|
|
38
|
+
resolved after the run — it does not prove the run *caused*
|
|
39
|
+
the resolution, so the stats never claim it did. An incident
|
|
40
|
+
that is still open, or was escalated to a human, is counted as
|
|
41
|
+
not resolved, never as a failure of the runbook.
|
|
42
|
+
|
|
43
|
+
How suggestions use the stats — the "influence on proposals"
|
|
44
|
+
the roadmap asks for, kept inside its proper bounds:
|
|
45
|
+
|
|
46
|
+
- Suggestions are ranked on the stats of *same-service* past
|
|
47
|
+
incidents only. Retrieval also returns neighbours — the same
|
|
48
|
+
namespace, a similar title — because a human may learn from
|
|
49
|
+
them; but a fix's track record may only influence a proposal
|
|
50
|
+
for the service it actually ran on, so neighbours never count
|
|
51
|
+
toward a suggestion.
|
|
52
|
+
- A suggestion is advice, Tier 1: it names the runbook, its
|
|
53
|
+
record on similar incidents, and the past incidents behind
|
|
54
|
+
that record. It runs nothing, proposes nothing through the
|
|
55
|
+
policy engine, and approves nothing — a human or a later
|
|
56
|
+
proposal step still takes any suggested fix through the
|
|
57
|
+
normal Tier 2 approval or Tier 3 grant. Memory can inform a
|
|
58
|
+
proposal; it can never become one by itself. (The
|
|
59
|
+
architecture doc already states this rule.)
|
|
60
|
+
- Only runbooks in the runbook library are ever suggested, and
|
|
61
|
+
only when at least one similar past incident actually ran
|
|
62
|
+
them. No history, no suggestion — memory never invents a
|
|
63
|
+
track record.
|
|
64
|
+
|
|
65
|
+
Scope, stated honestly: memory here is in-process and seeded
|
|
66
|
+
for the demo (``demo_memory``, ``demo_incident_memory``).
|
|
67
|
+
Since Phase 7 it can also be wired into the live path: an
|
|
68
|
+
operator may hand an ``IncidentMemory`` to the reasoning loop
|
|
69
|
+
(or to the server, which passes it on), and the loop then
|
|
70
|
+
attaches this module's report to the incident it diagnosed —
|
|
71
|
+
after the diagnosis is decided, as advice only. The report is
|
|
72
|
+
never an input to a claim, a cause, a proposal or an action,
|
|
73
|
+
and a memory that fails is skipped, never fatal. Suggestions
|
|
74
|
+
are also still reported on their own, through ``sre-agent
|
|
75
|
+
memory-demo``. Nothing here claims otherwise.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
from __future__ import annotations
|
|
79
|
+
|
|
80
|
+
import re
|
|
81
|
+
|
|
82
|
+
from .logging_config import get_logger
|
|
83
|
+
from .models import (
|
|
84
|
+
Evidence,
|
|
85
|
+
Incident,
|
|
86
|
+
IncidentStatus,
|
|
87
|
+
MemoryReport,
|
|
88
|
+
PastIncidentMatch,
|
|
89
|
+
RunbookStats,
|
|
90
|
+
RunbookSuggestion,
|
|
91
|
+
Target,
|
|
92
|
+
)
|
|
93
|
+
from .runbooks import RUNBOOK_PARAMETERS
|
|
94
|
+
|
|
95
|
+
# The report models live in models.py (an incident record can
|
|
96
|
+
# carry a report); they are re-exported here so existing
|
|
97
|
+
# imports from this module keep working.
|
|
98
|
+
__all__ = [
|
|
99
|
+
"IncidentMemory",
|
|
100
|
+
"MemoryReport",
|
|
101
|
+
"PastIncidentMatch",
|
|
102
|
+
"RunbookStats",
|
|
103
|
+
"RunbookSuggestion",
|
|
104
|
+
"demo_incident_memory",
|
|
105
|
+
"demo_memory",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
log = get_logger(__name__)
|
|
109
|
+
|
|
110
|
+
# The fixed retrieval points. Same service is the strongest
|
|
111
|
+
# signal — "last time this service broke" is the question being
|
|
112
|
+
# answered — then where it lives, then the words the two records
|
|
113
|
+
# share, capped so wording can never outweigh the target.
|
|
114
|
+
POINTS_SAME_SERVICE = 4
|
|
115
|
+
POINTS_SAME_NAMESPACE = 2
|
|
116
|
+
POINTS_SAME_ENVIRONMENT = 1
|
|
117
|
+
POINTS_PER_SHARED_WORD = 1
|
|
118
|
+
MAX_WORD_POINTS = 3
|
|
119
|
+
# A match must be worth reporting: same environment alone (1) or
|
|
120
|
+
# a couple of shared words alone is not a memory, it is a guess.
|
|
121
|
+
MIN_MATCH_SCORE = 3
|
|
122
|
+
|
|
123
|
+
_WORD_RE = re.compile(r"[a-z0-9]+")
|
|
124
|
+
_STOP_WORDS = {
|
|
125
|
+
"a", "an", "and", "are", "as", "at", "be", "by", "for", "from",
|
|
126
|
+
"in", "is", "it", "of", "on", "or", "the", "to", "was", "were",
|
|
127
|
+
"with", "this", "that", "its",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _words(text: str) -> set[str]:
|
|
132
|
+
return {w for w in _WORD_RE.findall(text.lower()) if w not in _STOP_WORDS and len(w) > 2}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _record_words(incident: Incident) -> set[str]:
|
|
136
|
+
words = _words(incident.title)
|
|
137
|
+
if incident.diagnosis:
|
|
138
|
+
words |= _words(incident.diagnosis)
|
|
139
|
+
for hyp in incident.hypotheses:
|
|
140
|
+
words |= _words(hyp.statement)
|
|
141
|
+
return words
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _action_runbook_ids(incident: Incident) -> list[str]:
|
|
145
|
+
"""The runbooks that ran on *incident*, from its own records.
|
|
146
|
+
|
|
147
|
+
A runbook run leaves an evidence record of kind ``action``
|
|
148
|
+
whose query begins ``runbook:<id> `` (built in
|
|
149
|
+
``runbooks.py``). Anything else is not a run and is ignored.
|
|
150
|
+
"""
|
|
151
|
+
ids: list[str] = []
|
|
152
|
+
for ev in incident.evidence:
|
|
153
|
+
if ev.kind != "action" or not ev.query.startswith("runbook:"):
|
|
154
|
+
continue
|
|
155
|
+
runbook_id = ev.query.removeprefix("runbook:").split(" ", 1)[0].strip()
|
|
156
|
+
if runbook_id:
|
|
157
|
+
ids.append(runbook_id)
|
|
158
|
+
return ids
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _score(query: Incident, past: Incident) -> tuple[int, list[str]]:
|
|
162
|
+
score = 0
|
|
163
|
+
reasons: list[str] = []
|
|
164
|
+
if past.target.service == query.target.service:
|
|
165
|
+
score += POINTS_SAME_SERVICE
|
|
166
|
+
reasons.append(f"same service ({query.target.service})")
|
|
167
|
+
if past.target.namespace == query.target.namespace:
|
|
168
|
+
score += POINTS_SAME_NAMESPACE
|
|
169
|
+
reasons.append(f"same namespace ({query.target.namespace})")
|
|
170
|
+
if past.target.environment == query.target.environment:
|
|
171
|
+
score += POINTS_SAME_ENVIRONMENT
|
|
172
|
+
reasons.append(f"same environment ({query.target.environment})")
|
|
173
|
+
shared = sorted(_record_words(query) & _record_words(past))
|
|
174
|
+
if shared:
|
|
175
|
+
points = min(len(shared) * POINTS_PER_SHARED_WORD, MAX_WORD_POINTS)
|
|
176
|
+
score += points
|
|
177
|
+
shown = ", ".join(shared[:MAX_WORD_POINTS])
|
|
178
|
+
reasons.append(f"shared words in the records: {shown} (+{points})")
|
|
179
|
+
return score, reasons
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _stats_for(incidents: list[Incident]) -> list[RunbookStats]:
|
|
183
|
+
by_id: dict[str, RunbookStats] = {}
|
|
184
|
+
for incident in incidents:
|
|
185
|
+
for runbook_id in _action_runbook_ids(incident):
|
|
186
|
+
stats = by_id.setdefault(runbook_id, RunbookStats(runbook_id=runbook_id))
|
|
187
|
+
stats.attempts += 1
|
|
188
|
+
if incident.status is IncidentStatus.RESOLVED:
|
|
189
|
+
stats.ended_resolved += 1
|
|
190
|
+
else:
|
|
191
|
+
stats.not_resolved += 1
|
|
192
|
+
return [by_id[key] for key in sorted(by_id)]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class IncidentMemory:
|
|
196
|
+
"""Past incidents, stored as copies, behind a small interface."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, incidents: list[Incident] | None = None) -> None:
|
|
199
|
+
self._incidents: list[Incident] = []
|
|
200
|
+
for incident in incidents or []:
|
|
201
|
+
self.add(incident)
|
|
202
|
+
|
|
203
|
+
def __len__(self) -> int:
|
|
204
|
+
return len(self._incidents)
|
|
205
|
+
|
|
206
|
+
def add(self, incident: Incident) -> None:
|
|
207
|
+
"""Store a copy of *incident* as a past incident."""
|
|
208
|
+
self._incidents.append(incident.model_copy(deep=True))
|
|
209
|
+
|
|
210
|
+
def incidents(self) -> list[Incident]:
|
|
211
|
+
"""Copies of everything held, oldest first."""
|
|
212
|
+
return [incident.model_copy(deep=True) for incident in self._incidents]
|
|
213
|
+
|
|
214
|
+
def find_similar(self, query: Incident, *, limit: int = 5) -> list[PastIncidentMatch]:
|
|
215
|
+
"""Past incidents similar to *query*, most similar first.
|
|
216
|
+
|
|
217
|
+
The query incident itself is never its own match. Only
|
|
218
|
+
scores at or above ``MIN_MATCH_SCORE`` are reported.
|
|
219
|
+
Ties are broken by incident id, so the order is stable
|
|
220
|
+
and a test can check it exactly.
|
|
221
|
+
"""
|
|
222
|
+
if limit < 1:
|
|
223
|
+
raise ValueError("limit must be at least 1")
|
|
224
|
+
matches: list[PastIncidentMatch] = []
|
|
225
|
+
for past in self._incidents:
|
|
226
|
+
if past.id == query.id:
|
|
227
|
+
continue
|
|
228
|
+
score, reasons = _score(query, past)
|
|
229
|
+
if score < MIN_MATCH_SCORE:
|
|
230
|
+
continue
|
|
231
|
+
matches.append(
|
|
232
|
+
PastIncidentMatch(
|
|
233
|
+
incident_id=past.id,
|
|
234
|
+
title=past.title,
|
|
235
|
+
target=past.target,
|
|
236
|
+
status=past.status,
|
|
237
|
+
diagnosis=past.diagnosis,
|
|
238
|
+
score=score,
|
|
239
|
+
reasons=reasons,
|
|
240
|
+
runbooks_run=_action_runbook_ids(past),
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
matches.sort(key=lambda m: (-m.score, m.incident_id))
|
|
244
|
+
return matches[:limit]
|
|
245
|
+
|
|
246
|
+
def runbook_stats(self) -> list[RunbookStats]:
|
|
247
|
+
"""Every runbook's record across all past incidents held."""
|
|
248
|
+
return _stats_for(self._incidents)
|
|
249
|
+
|
|
250
|
+
def suggest_runbooks(self, query: Incident, *, limit: int = 3) -> list[RunbookSuggestion]:
|
|
251
|
+
"""Runbooks worth considering for *query*, from same-service past.
|
|
252
|
+
|
|
253
|
+
Only matches on the query's own service count toward a
|
|
254
|
+
suggestion (see the module note: neighbours are shown for
|
|
255
|
+
reading, never counted here). Ranked by resolved share on
|
|
256
|
+
those incidents, then by how many runs back that share,
|
|
257
|
+
then by runbook id — a fixed order a test can check.
|
|
258
|
+
Advice only: this builds no proposal and runs nothing.
|
|
259
|
+
"""
|
|
260
|
+
if limit < 1:
|
|
261
|
+
raise ValueError("limit must be at least 1")
|
|
262
|
+
matches = self.find_similar(query, limit=len(self._incidents) or 1)
|
|
263
|
+
match_ids = {
|
|
264
|
+
m.incident_id for m in matches if m.target.service == query.target.service
|
|
265
|
+
}
|
|
266
|
+
similar = [i for i in self._incidents if i.id in match_ids]
|
|
267
|
+
suggestions: list[RunbookSuggestion] = []
|
|
268
|
+
for stats in _stats_for(similar):
|
|
269
|
+
if stats.runbook_id not in RUNBOOK_PARAMETERS:
|
|
270
|
+
continue
|
|
271
|
+
supporting = [
|
|
272
|
+
incident.id
|
|
273
|
+
for incident in similar
|
|
274
|
+
if stats.runbook_id in _action_runbook_ids(incident)
|
|
275
|
+
]
|
|
276
|
+
suggestions.append(
|
|
277
|
+
RunbookSuggestion(
|
|
278
|
+
runbook_id=stats.runbook_id,
|
|
279
|
+
attempts_on_similar=stats.attempts,
|
|
280
|
+
ended_resolved_on_similar=stats.ended_resolved,
|
|
281
|
+
success_rate_on_similar=stats.success_rate,
|
|
282
|
+
supporting_incident_ids=sorted(supporting),
|
|
283
|
+
reason=(
|
|
284
|
+
f"on same-service past incidents it ran {stats.attempts} time(s) and "
|
|
285
|
+
f"{stats.ended_resolved} of those incidents ended resolved "
|
|
286
|
+
f"({stats.success_rate:.0%}); past incident(s): "
|
|
287
|
+
f"{', '.join(sorted(supporting))}"
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
suggestions.sort(
|
|
292
|
+
key=lambda s: (-s.success_rate_on_similar, -s.attempts_on_similar, s.runbook_id)
|
|
293
|
+
)
|
|
294
|
+
return suggestions[:limit]
|
|
295
|
+
|
|
296
|
+
def report(self, query: Incident) -> MemoryReport:
|
|
297
|
+
"""One report: similar past, overall stats, suggestions."""
|
|
298
|
+
report = MemoryReport(
|
|
299
|
+
query_incident_id=query.id,
|
|
300
|
+
query_target=query.target,
|
|
301
|
+
past_incidents_held=len(self._incidents),
|
|
302
|
+
matches=self.find_similar(query),
|
|
303
|
+
overall_stats=self.runbook_stats(),
|
|
304
|
+
suggestions=self.suggest_runbooks(query),
|
|
305
|
+
)
|
|
306
|
+
log.info(
|
|
307
|
+
"memory report for incident %s: %d match(es), %d suggestion(s)",
|
|
308
|
+
query.id,
|
|
309
|
+
len(report.matches),
|
|
310
|
+
len(report.suggestions),
|
|
311
|
+
)
|
|
312
|
+
return report
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _past_incident(
|
|
316
|
+
incident_id: str,
|
|
317
|
+
*,
|
|
318
|
+
service: str,
|
|
319
|
+
title: str,
|
|
320
|
+
diagnosis: str,
|
|
321
|
+
runbook_id: str,
|
|
322
|
+
resolved: bool,
|
|
323
|
+
) -> Incident:
|
|
324
|
+
"""One seeded past incident, walked through its real life cycle."""
|
|
325
|
+
target = Target(environment="demo", namespace="shop", service=service)
|
|
326
|
+
incident = Incident(id=incident_id, title=title, target=target)
|
|
327
|
+
incident.transition(IncidentStatus.TRIAGING)
|
|
328
|
+
incident.diagnosis = diagnosis
|
|
329
|
+
incident.transition(IncidentStatus.DIAGNOSED)
|
|
330
|
+
incident.add_evidence(
|
|
331
|
+
Evidence(
|
|
332
|
+
kind="action",
|
|
333
|
+
query=f"runbook:{runbook_id} on {service}/shop (demo)",
|
|
334
|
+
summary=f"{runbook_id} ran on {service} in the demo.",
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
if resolved:
|
|
338
|
+
incident.transition(IncidentStatus.MITIGATING)
|
|
339
|
+
incident.transition(IncidentStatus.RESOLVED)
|
|
340
|
+
else:
|
|
341
|
+
incident.transition(IncidentStatus.ESCALATED)
|
|
342
|
+
return incident
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def demo_incident_memory() -> IncidentMemory:
|
|
346
|
+
"""The seeded demo past, as a memory a caller can wire in.
|
|
347
|
+
|
|
348
|
+
The same four past incidents ``demo_memory`` reports on,
|
|
349
|
+
handed back as the memory itself, so the reasoning loop or
|
|
350
|
+
the server can be built with it (the shipped server app is)
|
|
351
|
+
and attach its advice to live demo diagnoses.
|
|
352
|
+
"""
|
|
353
|
+
return IncidentMemory(
|
|
354
|
+
[
|
|
355
|
+
_past_incident(
|
|
356
|
+
"past-checkout-1",
|
|
357
|
+
service="checkout",
|
|
358
|
+
title="HighErrorRate on checkout",
|
|
359
|
+
diagnosis="Most likely: a call that checkout depends on is timing out",
|
|
360
|
+
runbook_id="restart-workload",
|
|
361
|
+
resolved=True,
|
|
362
|
+
),
|
|
363
|
+
_past_incident(
|
|
364
|
+
"past-checkout-2",
|
|
365
|
+
service="checkout",
|
|
366
|
+
title="Checkout errors are above 10%",
|
|
367
|
+
diagnosis="Most likely: checkout is failing on some requests",
|
|
368
|
+
runbook_id="restart-workload",
|
|
369
|
+
resolved=True,
|
|
370
|
+
),
|
|
371
|
+
_past_incident(
|
|
372
|
+
"past-checkout-3",
|
|
373
|
+
service="checkout",
|
|
374
|
+
title="Checkout errors are above 10%",
|
|
375
|
+
diagnosis="Most likely: checkout is failing on some requests",
|
|
376
|
+
runbook_id="scale-replicas",
|
|
377
|
+
resolved=False,
|
|
378
|
+
),
|
|
379
|
+
_past_incident(
|
|
380
|
+
"past-search-1",
|
|
381
|
+
service="search",
|
|
382
|
+
title="Search errors are above 10%",
|
|
383
|
+
diagnosis="Most likely: search is failing on some requests",
|
|
384
|
+
runbook_id="restart-workload",
|
|
385
|
+
resolved=True,
|
|
386
|
+
),
|
|
387
|
+
]
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def demo_memory() -> MemoryReport:
|
|
392
|
+
"""A seeded memory report for a new checkout incident.
|
|
393
|
+
|
|
394
|
+
The past is the four demo incidents held by
|
|
395
|
+
``demo_incident_memory``: checkout broke twice and a
|
|
396
|
+
restart was run both times, and both incidents ended
|
|
397
|
+
resolved; checkout broke once more, a scale was run, and
|
|
398
|
+
that incident was escalated, not resolved; and the search
|
|
399
|
+
service broke once, a restart was run, and it ended
|
|
400
|
+
resolved. The query is a fresh checkout incident, so the
|
|
401
|
+
report shows retrieval favouring the same service, overall
|
|
402
|
+
restart stats of 3 runs and 3 resolved, and a suggestion
|
|
403
|
+
order — restart before scale — that comes only from the
|
|
404
|
+
same-service (checkout) past, not from the search record.
|
|
405
|
+
"""
|
|
406
|
+
query = Incident(
|
|
407
|
+
id="new-checkout-1",
|
|
408
|
+
title="Checkout errors are above 10%",
|
|
409
|
+
target=Target(environment="demo", namespace="shop", service="checkout"),
|
|
410
|
+
)
|
|
411
|
+
return demo_incident_memory().report(query)
|