detectkit 0.23.1__tar.gz → 0.24.0__tar.gz

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.
Files changed (126) hide show
  1. {detectkit-0.23.1/detectkit.egg-info → detectkit-0.24.0}/PKG-INFO +1 -1
  2. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/__init__.py +1 -1
  3. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/config_emitter.py +17 -5
  4. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/html_labeler.py +36 -8
  5. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/label_server.py +14 -6
  6. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/rules/autotune.md +4 -2
  7. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +3 -2
  8. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/autotune.py +4 -4
  9. {detectkit-0.23.1 → detectkit-0.24.0/detectkit.egg-info}/PKG-INFO +1 -1
  10. {detectkit-0.23.1 → detectkit-0.24.0}/LICENSE +0 -0
  11. {detectkit-0.23.1 → detectkit-0.24.0}/MANIFEST.in +0 -0
  12. {detectkit-0.23.1 → detectkit-0.24.0}/README.md +0 -0
  13. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/__init__.py +0 -0
  14. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/__init__.py +0 -0
  15. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/base.py +0 -0
  16. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/branding.py +0 -0
  17. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/email.py +0 -0
  18. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/factory.py +0 -0
  19. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/mattermost.py +0 -0
  20. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/slack.py +0 -0
  21. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/telegram.py +0 -0
  22. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/channels/webhook.py +0 -0
  23. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
  24. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/_base.py +0 -0
  25. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
  26. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
  27. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
  28. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
  29. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/_types.py +0 -0
  30. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
  31. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/__init__.py +0 -0
  32. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/_base.py +0 -0
  33. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/_types.py +0 -0
  34. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/autotuner.py +0 -0
  35. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/crossval.py +0 -0
  36. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/detector_select.py +0 -0
  37. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/distribution.py +0 -0
  38. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/grid_search.py +0 -0
  39. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/labels.py +0 -0
  40. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/result.py +0 -0
  41. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/scoring.py +0 -0
  42. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/seasonality_search.py +0 -0
  43. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/settings.py +0 -0
  44. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/autotune/window_select.py +0 -0
  45. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/__init__.py +0 -0
  46. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/_output.py +0 -0
  47. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
  48. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
  49. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/rules/cli.md +0 -0
  50. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/rules/detectors.md +0 -0
  51. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
  52. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
  53. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
  54. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
  55. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
  56. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
  57. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/__init__.py +0 -0
  58. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/clean.py +0 -0
  59. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/init.py +0 -0
  60. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/init_claude.py +0 -0
  61. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/run.py +0 -0
  62. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/test_alert.py +0 -0
  63. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/commands/unlock.py +0 -0
  64. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/cli/main.py +0 -0
  65. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/config/__init__.py +0 -0
  66. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/config/metric_config.py +0 -0
  67. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/config/profile.py +0 -0
  68. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/config/project_config.py +0 -0
  69. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/config/validator.py +0 -0
  70. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/core/__init__.py +0 -0
  71. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/core/interval.py +0 -0
  72. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/core/models.py +0 -0
  73. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/__init__.py +0 -0
  74. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/_sql_manager.py +0 -0
  75. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/clickhouse_manager.py +0 -0
  76. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/__init__.py +0 -0
  77. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
  78. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
  79. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_base.py +0 -0
  80. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
  81. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_detections.py +0 -0
  82. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
  83. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_metrics.py +0 -0
  84. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_schema.py +0 -0
  85. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/_tasks.py +0 -0
  86. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/internal_tables/manager.py +0 -0
  87. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/manager.py +0 -0
  88. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/mysql_manager.py +0 -0
  89. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/postgres_manager.py +0 -0
  90. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/database/tables.py +0 -0
  91. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/__init__.py +0 -0
  92. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/base.py +0 -0
  93. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/factory.py +0 -0
  94. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/seasonality.py +0 -0
  95. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/statistical/__init__.py +0 -0
  96. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/statistical/_windowed.py +0 -0
  97. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/statistical/iqr.py +0 -0
  98. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/statistical/mad.py +0 -0
  99. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
  100. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/detectors/statistical/zscore.py +0 -0
  101. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/loaders/__init__.py +0 -0
  102. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/loaders/metric_loader.py +0 -0
  103. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/loaders/query_template.py +0 -0
  104. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/__init__.py +0 -0
  105. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/error_dispatch.py +0 -0
  106. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
  107. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
  108. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/task_manager/_base.py +0 -0
  109. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
  110. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
  111. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/task_manager/_types.py +0 -0
  112. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/orchestration/task_manager/manager.py +0 -0
  113. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/utils/__init__.py +0 -0
  114. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/utils/datetime_utils.py +0 -0
  115. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/utils/env_interpolation.py +0 -0
  116. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/utils/json_utils.py +0 -0
  117. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit/utils/stats.py +0 -0
  118. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit.egg-info/SOURCES.txt +0 -0
  119. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit.egg-info/dependency_links.txt +0 -0
  120. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit.egg-info/entry_points.txt +0 -0
  121. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit.egg-info/requires.txt +0 -0
  122. {detectkit-0.23.1 → detectkit-0.24.0}/detectkit.egg-info/top_level.txt +0 -0
  123. {detectkit-0.23.1 → detectkit-0.24.0}/pyproject.toml +0 -0
  124. {detectkit-0.23.1 → detectkit-0.24.0}/requirements.txt +0 -0
  125. {detectkit-0.23.1 → detectkit-0.24.0}/setup.cfg +0 -0
  126. {detectkit-0.23.1 → detectkit-0.24.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.23.1
3
+ Version: 0.24.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
4
4
  A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
5
5
  """
6
6
 
7
- __version__ = "0.23.1"
7
+ __version__ = "0.24.0"
8
8
 
9
9
  from detectkit.core.interval import Interval
10
10
  from detectkit.core.models import ColumnDefinition, TableModel
@@ -85,11 +85,23 @@ def _build_body(original: MetricConfig, result: AutoTuneResult, new_name: str) -
85
85
  body["loading_start_time"] = original.loading_start_time
86
86
  body["loading_batch_size"] = original.loading_batch_size
87
87
 
88
- scalar_cols = _flatten_scalar_seasonality(result.chosen_seasonality)
89
- if scalar_cols:
90
- body["seasonality_columns"] = scalar_cols
91
- elif original.seasonality_columns:
92
- body["seasonality_columns"] = original.seasonality_columns
88
+ # The top-level `seasonality_columns` field feeds the *built-in* time-feature
89
+ # extractor and is validated against the built-in allowlist (hour/day_of_week/…).
90
+ # When the metric instead sources seasonality from the query
91
+ # (`query_columns.seasonality`, e.g. a custom `league_day`), the loader ignores
92
+ # `seasonality_columns` entirely and the chosen grouping rides in the detector's
93
+ # `seasonality_components`. So never copy the chosen columns here in that mode —
94
+ # they may be custom names the validator rejects, and they are already declared
95
+ # in the preserved `query_columns`.
96
+ if original.query_columns and original.query_columns.seasonality:
97
+ if original.seasonality_columns:
98
+ body["seasonality_columns"] = original.seasonality_columns
99
+ else:
100
+ scalar_cols = _flatten_scalar_seasonality(result.chosen_seasonality)
101
+ if scalar_cols:
102
+ body["seasonality_columns"] = scalar_cols
103
+ elif original.seasonality_columns:
104
+ body["seasonality_columns"] = original.seasonality_columns
93
105
 
94
106
  body["detectors"] = [
95
107
  {"type": result.chosen_detector_type, "params": result.chosen_detector_params}
@@ -7,7 +7,8 @@ file in the canonical schema, fed back via
7
7
  ``dtk autotune --select <metric> --incidents <file-or-dir>``.
8
8
 
9
9
  The page is offline-only — a browser cannot write to the project, so Export
10
- downloads a **versioned** file (``<metric>-<UTC-stamp>.yml``); drop it into
10
+ downloads a **versioned** file named after the metric, with the optional set name
11
+ folded in as a suffix (``<metric>[-<name>]-<UTC-stamp>.yml``); drop it into
11
12
  ``incidents/<metric>/`` to keep every labeling round (``--incidents`` accepts
12
13
  that directory and uses the newest version).
13
14
 
@@ -58,6 +59,11 @@ _TEMPLATE = """<!doctype html>
58
59
  .brand span { color: var(--faint); font-size: 12px; }
59
60
  h1 { font-size: 18px; line-height: 1.3; margin: 0 0 6px; color: var(--paper); font-weight: 600; }
60
61
  h1 code { color: var(--clay); font-family: var(--mono); font-size: .82em; }
62
+ .ichip { display:inline-flex; align-items:center; gap:6px; vertical-align: middle; margin-left: 8px;
63
+ font-family: var(--mono); font-size: 12px; font-weight: 500; color: var(--paper);
64
+ background: rgba(209,91,54,0.16); border: 1px solid var(--clay); border-radius: 999px; padding: 3px 10px; }
65
+ .ichip .d { width:6px; height:6px; border-radius:50%; background: var(--clay); }
66
+ .ichip b { color: var(--clay); font-weight: 700; }
61
67
  .hint { color: var(--faint); font-size: 13px; margin: 0 0 18px; line-height: 1.55; }
62
68
  .hint code, code.k { color: var(--term-text); font-family: var(--mono); font-size: 12px;
63
69
  background: var(--term-surface); border: 1px solid var(--term-border); border-radius: 5px; padding: 1px 6px; }
@@ -106,7 +112,8 @@ _TEMPLATE = """<!doctype html>
106
112
  <svg viewBox="0 0 100 100" aria-hidden="true"><rect x="3" y="3" width="94" height="94" rx="26" fill="#D15B36"/><polyline points="14,62 36,62 50,22 64,62 86,62" fill="none" stroke="#FBF9F3" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="50" cy="22" r="6.5" fill="#FBF9F3"/></svg>
107
113
  <b>detectkit</b><span>· incident labeler</span>
108
114
  </div>
109
- <h1>Label incidents — <code>__METRIC__</code></h1>
115
+ <h1>Label incidents — <code>__METRIC__</code><span id="intervalchip" class="ichip"
116
+ title="The metric's sampling interval — the spacing between points, taken straight from the metric."></span></h1>
110
117
  <p class="hint">Click-drag across the chart to mark each real incident, add a short description, then
111
118
  <b>Export</b>. Save the file into <code class="k">incidents/__METRIC__/</code> and run
112
119
  <code class="k">dtk autotune --select __METRIC__ --incidents incidents/__METRIC__/</code></p>
@@ -129,7 +136,7 @@ _TEMPLATE = """<!doctype html>
129
136
  <div id="empty" class="empty">No incidents marked yet — drag across a span on the chart above.</div>
130
137
  <ul id="list"></ul>
131
138
  <footer>All times UTC · self-contained, nothing leaves your browser · re-label any time —
132
- exports are versioned (<code>__METRIC__-&lt;timestamp&gt;.yml</code>), so keep every round in
139
+ exports are versioned (<code>__METRIC__[-&lt;name&gt;]-&lt;timestamp&gt;.yml</code>), so keep every round in
133
140
  <code>incidents/__METRIC__/</code>. Generated by <code>dtk autotune --label</code>.</footer>
134
141
  </div>
135
142
  <script>
@@ -138,6 +145,9 @@ const DATA = __PAYLOAD__;
138
145
  // and Export POSTs straight into incidents/<metric>/. As a static file it is null,
139
146
  // and Export falls back to a browser download.
140
147
  const SAVE_URL = __SAVE_URL__;
148
+ // The metric's sampling interval (seconds). Passed straight from the metric when
149
+ // known; otherwise inferred from the median spacing of points.
150
+ const INTERVAL_S = __INTERVAL__;
141
151
  const pts = DATA.points.map(p => ({ts: Date.parse(p.t.replace(' ','T')+'Z'), v: p.v}));
142
152
  const N = pts.length;
143
153
  const vraw = pts.filter(p => p.v !== null).map(p => p.v);
@@ -442,10 +452,13 @@ exportBtn.onclick = () => {
442
452
  const d=new Date();
443
453
  const stamp=d.getUTCFullYear()+pad2(d.getUTCMonth()+1)+pad2(d.getUTCDate())+'T'
444
454
  +pad2(d.getUTCHours())+pad2(d.getUTCMinutes())+pad2(d.getUTCSeconds())+'Z';
445
- const base = name.trim() ? slug(name) : '__METRIC__';
455
+ // Always name the file after the metric; the optional set name is a suffix.
456
+ const s = name.trim() ? slug(name) : '';
457
+ const suffix = (s && s !== '__METRIC__') ? '-' + s : '';
458
+ const fname = '__METRIC__' + suffix + '-' + stamp + '.yml';
446
459
  const blob=new Blob([y], {type:'text/yaml'}); const a=document.createElement('a');
447
- a.href=URL.createObjectURL(blob); a.download=base+'-'+stamp+'.yml'; a.click();
448
- setMsg('Downloaded ' + base + '-' + stamp + '.yml — move it into incidents/__METRIC__/ and re-run.', 'info');
460
+ a.href=URL.createObjectURL(blob); a.download=fname; a.click();
461
+ setMsg('Downloaded ' + fname + ' — move it into incidents/__METRIC__/ and re-run.', 'info');
449
462
  }
450
463
  };
451
464
 
@@ -455,19 +468,33 @@ function drawAll() { draw(); drawOverview();
455
468
  function fit() { dpr = window.devicePixelRatio || 1;
456
469
  c.width=c.clientWidth*dpr; c.height=c.clientHeight*dpr;
457
470
  ov.width=ov.clientWidth*dpr; ov.height=ov.clientHeight*dpr; drawAll(); }
471
+ function fmtInterval(s) { if (s<=0) return '?';
472
+ if (s%86400===0) return (s/86400)+'d'; if (s%3600===0) return (s/3600)+'h';
473
+ if (s%60===0) return (s/60)+'min'; return s+'s'; }
474
+ function medianIntervalSec() { if (N<2) return 0;
475
+ const d=[]; for (let i=1;i<N;i++) d.push(pts[i].ts-pts[i-1].ts);
476
+ d.sort((a,b)=>a-b); return Math.round(d[Math.floor(d.length/2)]/1000); }
477
+ const intervalSec = (typeof INTERVAL_S==='number' && INTERVAL_S>0) ? INTERVAL_S : medianIntervalSec();
478
+ document.getElementById('intervalchip').innerHTML =
479
+ '<span class="d"></span>interval <b>'+fmtInterval(intervalSec)+'</b>';
458
480
  window.addEventListener('resize', fit); fit(); render();
459
481
  </script>
460
482
  """
461
483
 
462
484
 
463
485
  def render_labeler_html(
464
- metric_name: str, data: dict[str, np.ndarray], *, save_url: str | None = None
486
+ metric_name: str,
487
+ data: dict[str, np.ndarray],
488
+ *,
489
+ save_url: str | None = None,
490
+ interval_seconds: int | None = None,
465
491
  ) -> str:
466
492
  """Return a self-contained HTML labeler page for *metric_name*'s series.
467
493
 
468
494
  With ``save_url`` (set by ``dtk autotune --label``'s local server) the page's
469
495
  Export button POSTs the labels straight to that endpoint; without it (a static
470
- file) Export falls back to a browser download.
496
+ file) Export falls back to a browser download. ``interval_seconds`` is the
497
+ metric's sampling interval shown as a chip (inferred from the data if omitted).
471
498
  """
472
499
  import json
473
500
 
@@ -481,5 +508,6 @@ def render_labeler_html(
481
508
  return (
482
509
  _TEMPLATE.replace("__PAYLOAD__", payload)
483
510
  .replace("__SAVE_URL__", json.dumps(save_url))
511
+ .replace("__INTERVAL__", json.dumps(interval_seconds))
484
512
  .replace("__METRIC__", metric_name)
485
513
  )
@@ -31,9 +31,12 @@ _MAX_BODY = 5_000_000 # generous cap on the posted labels payload
31
31
 
32
32
 
33
33
  def _sanitize(name: str) -> str:
34
- """Filesystem-safe slug for a label-set name; falls back to ``incidents``."""
35
- slug = _NAME_RE.sub("-", name.strip().lower()).strip("-")
36
- return slug or "incidents"
34
+ """Filesystem-safe slug for a label-set name; ``""`` when blank.
35
+
36
+ Used as an optional *suffix* appended to the metric-named filename, so a blank
37
+ name simply yields no suffix (the file is named after the metric alone).
38
+ """
39
+ return _NAME_RE.sub("-", name.strip().lower()).strip("-")
37
40
 
38
41
 
39
42
  def _stamp() -> str:
@@ -87,14 +90,16 @@ class _Handler(BaseHTTPRequestHandler):
87
90
 
88
91
  payload = json.loads(self.rfile.read(length).decode("utf-8"))
89
92
  yaml_text = str(payload.get("yaml", ""))
90
- set_name = _sanitize(str(payload.get("name", "")))
93
+ suffix = _sanitize(str(payload.get("name", "")))
91
94
  raw = _yaml.safe_load(yaml_text)
92
95
  # validate against the canonical schema before writing anything
93
96
  parse_incident_labels(
94
97
  raw, interval_seconds=srv.interval_seconds, metric_name=srv.metric
95
98
  )
96
99
  srv.incidents_dir.mkdir(parents=True, exist_ok=True)
97
- out = srv.incidents_dir / f"{set_name}-{_stamp()}.yml"
100
+ # Always name the file after the metric; the optional set name is a suffix.
101
+ stem = f"{srv.metric}-{suffix}" if suffix else srv.metric
102
+ out = srv.incidents_dir / f"{stem}-{_stamp()}.yml"
98
103
  out.write_text(yaml_text, encoding="utf-8")
99
104
  srv.saved_path = out
100
105
  except Exception as exc:
@@ -126,7 +131,10 @@ def build_label_server(
126
131
  server.incidents_dir = incidents_dir
127
132
  server.interval_seconds = interval_seconds
128
133
  server.html = render_labeler_html(
129
- metric_name, data, save_url=f"http://127.0.0.1:{port}/save?token={token}"
134
+ metric_name,
135
+ data,
136
+ save_url=f"http://127.0.0.1:{port}/save?token={token}",
137
+ interval_seconds=interval_seconds,
130
138
  )
131
139
  return server, f"http://127.0.0.1:{port}/?token={token}"
132
140
 
@@ -53,7 +53,8 @@ dtk autotune --select <sel> [--incidents FILE] [--label] [--scoring METRIC] \
53
53
  non-interactively) tunes **unsupervised**.
54
54
  - `--label` — open the interactive labeler (zoom/pan, edit incident edges,
55
55
  per-incident descriptions, named sets). **Default:** a local 127.0.0.1 server +
56
- browser; **Save & tune** writes `incidents/<name>/<set>-<UTC>.yml` and the run
56
+ browser; **Save & tune** writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml`
57
+ (named after the metric, optional set name as a suffix) and the run
57
58
  **continues into tuning on it**. `--no-serve` writes a static
58
59
  `metrics/<name>__labeler.html` (Export downloads the file) and exits; `--no-open`
59
60
  prints the URL instead of launching a browser.
@@ -91,7 +92,8 @@ user to recall timestamps** — it is the easiest, most reliable path:
91
92
  2. The user marks incidents on the chart (scroll to zoom, drag the navigator to
92
93
  move, click-drag to mark, drag an incident's edges to adjust, add a
93
94
  description, optionally name the set), then clicks **Save & tune**.
94
- 3. That writes `incidents/<name>/<set>-<UTC>.yml` automatically (versioned —
95
+ 3. That writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml` automatically
96
+ (named after the metric, optional set name as a suffix; versioned —
95
97
  re-labeling never overwrites) and the **same command continues into the tuning
96
98
  run** on it. No manual file moving.
97
99
  4. To re-tune later on saved sets, point `--incidents` at the folder
@@ -96,8 +96,9 @@ user through it:
96
96
  with an optional **description**). Adjust one by dragging its **edges**, or its
97
97
  **middle** to move it; optionally **name the set**; *remove* / *Clear all* fix
98
98
  mistakes.
99
- 3. Click **Save & tune**. The server writes `incidents/<name>/<set>-<UTC>.yml`
100
- automatically (versioned re-labeling never overwrites) and the **same command
99
+ 3. Click **Save & tune**. The server writes `incidents/<metric>/<metric>[-<set>]-<UTC>.yml`
100
+ automatically (named after the metric, with the optional set name as a suffix;
101
+ versioned — re-labeling never overwrites) and the **same command
101
102
  continues into the tuning run on it**. No manual file moving.
102
103
 
103
104
  (`--no-serve` is the offline fallback: it writes a static
@@ -100,9 +100,9 @@ _LABELS_GLOBS = ("*.yml", "*.yaml", "*.json")
100
100
  def _labels_files(directory: Path) -> list[Path]:
101
101
  """All labels files in *directory*, oldest→newest.
102
102
 
103
- The labeler saves versioned, ISO-stamped names (``<set>-<UTC>.yml``) which sort
104
- chronologically, so name order is chronological (tie-broken by mtime). This
105
- lets ``incidents/<metric>/`` keep every labeling round on disk.
103
+ The labeler saves versioned, ISO-stamped names (``<metric>[-<set>]-<UTC>.yml``)
104
+ which sort chronologically, so name order is chronological (tie-broken by mtime).
105
+ This lets ``incidents/<metric>/`` keep every labeling round on disk.
106
106
  """
107
107
  files: list[Path] = []
108
108
  for pattern in _LABELS_GLOBS:
@@ -348,7 +348,7 @@ def _tune_one(
348
348
  if label:
349
349
  click.echo(click.style(f"Processing metric: {name}", fg="cyan", bold=True))
350
350
  if no_serve:
351
- html = render_labeler_html(name, data)
351
+ html = render_labeler_html(name, data, interval_seconds=interval_seconds)
352
352
  out = project_root / "metrics" / f"{metric_path.stem}__labeler.html"
353
353
  out.write_text(html, encoding="utf-8")
354
354
  click.echo(f" Wrote labeler: {out.relative_to(project_root)}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detectkit
3
- Version: 0.23.1
3
+ Version: 0.24.0
4
4
  Summary: Metric monitoring with automatic anomaly detection
5
5
  Author: detectkit team
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes