sgnmon 0.1.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 (49) hide show
  1. sgnmon-0.1.0/.gitignore +11 -0
  2. sgnmon-0.1.0/.gitlab-ci.yml +67 -0
  3. sgnmon-0.1.0/CHANGELOG.md +19 -0
  4. sgnmon-0.1.0/CONSOLIDATION-TRACKING.md +180 -0
  5. sgnmon-0.1.0/CONSOLIDATION.md +273 -0
  6. sgnmon-0.1.0/LICENSE +195 -0
  7. sgnmon-0.1.0/Makefile +42 -0
  8. sgnmon-0.1.0/PKG-INFO +196 -0
  9. sgnmon-0.1.0/PROMQL-GRAFANA-NOTES.md +135 -0
  10. sgnmon-0.1.0/README.md +128 -0
  11. sgnmon-0.1.0/VM-PILOT-PLAN.md +125 -0
  12. sgnmon-0.1.0/docs/gen_ref_nav.py +58 -0
  13. sgnmon-0.1.0/docs/index.md +64 -0
  14. sgnmon-0.1.0/docs/tutorials/getting-started.md +114 -0
  15. sgnmon-0.1.0/docs/user/attaching.md +176 -0
  16. sgnmon-0.1.0/docs/user/custom-metrics.md +99 -0
  17. sgnmon-0.1.0/docs/user/health.md +144 -0
  18. sgnmon-0.1.0/docs/user/index.md +25 -0
  19. sgnmon-0.1.0/docs/user/metrics.md +131 -0
  20. sgnmon-0.1.0/docs/user/server.md +122 -0
  21. sgnmon-0.1.0/mkdocs.yml +69 -0
  22. sgnmon-0.1.0/pyproject.toml +213 -0
  23. sgnmon-0.1.0/src/sgnmon/__init__.py +62 -0
  24. sgnmon-0.1.0/src/sgnmon/_version.py +24 -0
  25. sgnmon-0.1.0/src/sgnmon/cli.py +256 -0
  26. sgnmon-0.1.0/src/sgnmon/elements.py +201 -0
  27. sgnmon-0.1.0/src/sgnmon/health.py +538 -0
  28. sgnmon-0.1.0/src/sgnmon/metrics.py +237 -0
  29. sgnmon-0.1.0/src/sgnmon/monitor.py +685 -0
  30. sgnmon-0.1.0/src/sgnmon/probes.py +776 -0
  31. sgnmon-0.1.0/src/sgnmon/py.typed +0 -0
  32. sgnmon-0.1.0/src/sgnmon/server.py +376 -0
  33. sgnmon-0.1.0/src/sgnmon/static/dashboard.css +219 -0
  34. sgnmon-0.1.0/src/sgnmon/static/dashboard.html +38 -0
  35. sgnmon-0.1.0/src/sgnmon/static/dashboard.js +963 -0
  36. sgnmon-0.1.0/src/sgnmon/tests/__init__.py +0 -0
  37. sgnmon-0.1.0/src/sgnmon/tests/helpers.py +169 -0
  38. sgnmon-0.1.0/src/sgnmon/tests/js/harness.js +431 -0
  39. sgnmon-0.1.0/src/sgnmon/tests/js/stubs.js +62 -0
  40. sgnmon-0.1.0/src/sgnmon/tests/test_browser.py +172 -0
  41. sgnmon-0.1.0/src/sgnmon/tests/test_cli.py +93 -0
  42. sgnmon-0.1.0/src/sgnmon/tests/test_composed.py +41 -0
  43. sgnmon-0.1.0/src/sgnmon/tests/test_dashboard.py +48 -0
  44. sgnmon-0.1.0/src/sgnmon/tests/test_elements.py +237 -0
  45. sgnmon-0.1.0/src/sgnmon/tests/test_health.py +357 -0
  46. sgnmon-0.1.0/src/sgnmon/tests/test_metrics.py +126 -0
  47. sgnmon-0.1.0/src/sgnmon/tests/test_monitor.py +230 -0
  48. sgnmon-0.1.0/src/sgnmon/tests/test_probes.py +221 -0
  49. sgnmon-0.1.0/src/sgnmon/tests/test_server.py +196 -0
@@ -0,0 +1,11 @@
1
+ *.egg-info
2
+ build/
3
+ dist/
4
+ site/
5
+ __pycache__
6
+ _version.py
7
+ .coverage
8
+ .vscode/
9
+ .idea/
10
+ *.patch
11
+ .claude/settings.local.json
@@ -0,0 +1,67 @@
1
+ include:
2
+ # -- python
3
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/python/sdist@1
4
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/python/wheel@1
5
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/python/code-quality@1
6
+ inputs:
7
+ analyzer: "ruff"
8
+ requirements: "-r requirements-lint.txt"
9
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/python/dependency-scanning@1
10
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/python/type-checking@1
11
+ inputs:
12
+ project_dir: "src"
13
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/python/test@1
14
+ inputs:
15
+ install_extra: test
16
+ python_versions:
17
+ - "3.11"
18
+ - "3.12"
19
+ - "3.13"
20
+ # test against sgn and sgn-ts from git (main)
21
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/python/test@1
22
+ inputs:
23
+ job_name: "python_test_latest"
24
+ install_extra: test
25
+ python_versions:
26
+ - "3.11"
27
+ - "3.12"
28
+ - "3.13"
29
+ # -- docs
30
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/mkdocs/build@1
31
+ inputs:
32
+ requirements: "-r requirements-docs.txt"
33
+ - component: $CI_SERVER_FQDN/computing/gitlab/components/mkdocs/pages@1
34
+ inputs:
35
+ pages_when: "default"
36
+
37
+ # -- customizations
38
+
39
+ # install latest sgn and sgn-ts from git (main) to catch any
40
+ # prerelease changes before they ship
41
+ python_test_latest:
42
+ before_script:
43
+ - pip install git+https://git.ligo.org/greg/sgn.git@main
44
+ - pip install git+https://git.ligo.org/greg/sgn-ts.git@main
45
+
46
+ ruff:
47
+ needs:
48
+ - requirements
49
+
50
+ mkdocs:
51
+ needs:
52
+ - requirements
53
+
54
+ # -- requirements
55
+ requirements:
56
+ stage: build
57
+ image: python:3.12
58
+ script:
59
+ - python -m pip install pipx
60
+ - pipx ensurepath
61
+ - source ~/.bashrc
62
+ - pipx install hatch
63
+ - hatch dep show requirements --feature docs > requirements-docs.txt
64
+ - hatch dep show requirements --feature lint > requirements-lint.txt
65
+ artifacts:
66
+ paths:
67
+ - requirements-*.txt
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2026-07-23
6
+
7
+ ### Added
8
+
9
+ - Initial release: monitoring for SGN pipelines — non-invasive taps and
10
+ in-graph elements feeding Prometheus metrics (data flow, latency,
11
+ per-pad execution time, freshness, EOS; opt-in process metrics), a
12
+ declarative custom-metrics API, per-event latency for event streams,
13
+ self-pacing health checks, and a monitoring server with a live
14
+ dashboard, `/metrics`, `/health`, `/status`, and server-sent events,
15
+ plus a CLI for zero-code attach (`sgnmon run`), demoing, and health
16
+ checks with plugin-friendly exit codes
17
+
18
+ [unreleased]: https://git.ligo.org/greg/sgnmon/-/compare/0.1.0...main
19
+ [0.1.0]: https://git.ligo.org/greg/sgnmon/-/tags/0.1.0
@@ -0,0 +1,180 @@
1
+ # Consolidation tracking
2
+
3
+ Live tracker for the remaining decisions and work in the monitoring
4
+ consolidation ([CONSOLIDATION.md](CONSOLIDATION.md)). Analysis and rationale
5
+ live there; this file only tracks state. Last updated 2026-07-22.
6
+
7
+ ## Done
8
+
9
+ - [x] Comparative analysis of sgnmon / sgn-skig / sgn testpoints (CONSOLIDATION.md §1–§5)
10
+ - [x] sgnmon gap-closing, four commits on `main` (§6):
11
+ - [x] `0b61ebe` pad execution-time histogram (`sgnmon_exec_seconds`)
12
+ - [x] `f97881e` opt-in process metrics (`Monitor(process_metrics=True)`)
13
+ - [x] `048f98e` custom metrics API (`MetricSpec`/`MetricsMixin`, `Monitor` factories)
14
+ - [x] `dbb144e` `EventLatency` + `PadProbe.record_latency`
15
+ - [x] Storage decision (§8): Prometheus stays the only query layer; single-node
16
+ VictoriaMetrics as long-term backend for both ops history (remote_write)
17
+ and sgnl scientific results (line-protocol push); InfluxDB, scald retention
18
+ policies, ginterval, and the Grafana exporter all retire
19
+ - [x] Health-check integration decision: monitor `/health` with stock Nagios
20
+ `check_http` (200 → OK, 503/refused → CRITICAL). No igwn-monitoring-plugins
21
+ packaging needed — it would put a Python sgnmon install on monitoring hosts
22
+ just to enrich alert text. Revisit only if on-call wants failing-check
23
+ names in the page itself; `sgnmon check` remains a dev/debug convenience.
24
+
25
+ ## Decisions open
26
+
27
+ - [x] **`IGWNAlertSource` home**: moved to **sgn-llai** (decided 2026-07-23,
28
+ new `sgnllai/sources/` package) — alert-driven low-latency work is
29
+ sgn-llai's domain, and repo-wide grep found zero current consumers
30
+ anywhere (verified twice: exact class name across all ligo repos, and
31
+ case-insensitive igwn-alert terms across sgnl/sgn-cal/sgn-ligo/
32
+ sgn-llai src+tests+bin+pyproject). Removed from sgn-ligo along with
33
+ its `igwn-alert` dependency — sgn-ligo's dependency delta from the
34
+ transport move is now just `igwn-auth-utils` + `jsonschema`;
35
+ `igwn-alert` is declared by sgn-llai instead.
36
+
37
+ - [x] **Home for sgn-skig's transport elements**: **sgn-ligo** (decided
38
+ 2026-07-22). Dependency delta is small — sgn-ligo already has
39
+ `confluent-kafka`, `sgn >= 0.8`, and `sgn-ts`; net-new declarations are
40
+ `igwn-alert` (lazy-imported by `IGWNAlertSource`), `igwn-auth-utils`
41
+ (SciToken discovery in `ScitokenKafkaSource`; likely already transitive
42
+ via gwpy, declare explicitly anyway), and `jsonschema` (`json_frames.py`,
43
+ which the Kafka elements use). skig's `pyyaml`/`requests` are used only
44
+ by the Grafana/pipeline layer and stay behind. Migration caveat:
45
+ `KafkaSource`/`KafkaSink`/`IGWNAlertSource`/`json_frames` are
46
+ metrics-free and move as-is, but `DelayBuffer`/`RoundRobinDistributor`
47
+ mix in skig's `MetricsCollectorMixin` — port their metrics to
48
+ `sgnmon.MetricsMixin` (adds a sgnmon dep wherever they land) or strip
49
+ them; `EventLatency` is NOT moved — already reimplemented in sgnmon.
50
+ Merge plan (decided 2026-07-22, supersedes the earlier rename idea):
51
+ - **Sources — merge.** `ScitokenKafkaSource` folds into `KafkaSource`
52
+ as an auth option (OAUTHBEARER token callback via lazy-imported
53
+ `igwn-auth-utils`); it only overrides consumer config. sgn-llai's
54
+ `run_superevent.py` already auto-picks between the two classes by
55
+ broker hostname — that logic becomes an `auth="auto"` default.
56
+ - **Sinks — merge on one base.** scald's kafka `Client.write()` is
57
+ just `json.dumps` + `produce(key=".".join(tags))` (~15 lines), so
58
+ both sinks are confluent-kafka JSON producers differing only in
59
+ payload schema and batching: sgn-ligo's writes scald-consumer format
60
+ (`{time: [...], data: [...]}`, interval batching; used by sgnl
61
+ `inspiral.py` + `ll_dq.py`), skig's writes generic per-frame JSON
62
+ (per-topic pads, lz4, delivery modes; used by sgn-llai). Plan
63
+ (2026-07-22, Olivia OK'd sgnl adapting): a single `KafkaSink` class,
64
+ confluent-kafka direct, generic JSON by default with a scald-format
65
+ payload mode (`{topic: {time, data}}` accumulation, interval flush,
66
+ tags-as-dotted-key) — no compatibility front-ends; sgnl's two call
67
+ sites update natively with the migration. Drops `ligo.scald` from
68
+ the kafka sink.
69
+ - Consumer census: sgnl uses sgnligo's scald-format sink; sgn-llai uses
70
+ skig's generic pair; **sgn-cal uses no Kafka at all**.
71
+ - Discovered: `sgnligo/sinks/influx_sink.py` is a *second* scald→Influx
72
+ writer outside skig (sgn-ligo keeps `ligo-scald` for it regardless).
73
+ Out of scope for the transport move, but it is another Influx client
74
+ to fold into the VM/Prometheus story eventually — added to
75
+ follow-ups.
76
+ - [ ] **Shared Grafana dashboard ownership**: one static dashboard with
77
+ `element`/`pad`/`job` variables replaces generated per-pipeline JSON.
78
+ Who builds/owns it, and where dashboards-as-code live in IGWN Grafana
79
+ provisioning. sgn-llai's custom-metric panels get hand-built once.
80
+ - [ ] **Manifest question**: does sgn-llai CI depend on skig's
81
+ `write_metrics_manifest()` YAML export? Dropped from the port; if needed,
82
+ it is a small addition to the sgnmon mixin. Ask sgnl folks before the
83
+ migration starts.
84
+
85
+ ## Work queue (in order)
86
+
87
+ - [ ] **VM prototype** — gates the §8 storage plan. Sysadmin handoff plan:
88
+ [VM-PILOT-PLAN.md](VM-PILOT-PLAN.md). PromQL/Grafana aggregation notes
89
+ (how query-time reduction replaces scald's write-time tiers):
90
+ [PROMQL-GRAFANA-NOTES.md](PROMQL-GRAFANA-NOTES.md). Push a few weeks of one
91
+ representative sgnl result series (include the sparsest one) into a
92
+ scratch VictoriaMetrics via InfluxDB line protocol; rebuild the Grafana
93
+ panel beside the Influx original. Verify: (a) GPS-vs-Unix timestamp
94
+ convention the scald dashboards assume (VM sample timestamps must be Unix;
95
+ GPS as value or companion series); (b) sparse series query shape
96
+ (`max_over_time(x[$__interval])` / points panels vs the ~5m gauge
97
+ staleness window); (c) whether any Influx data is event *records* rather
98
+ than time series — those belong in a proper service (e.g. GraceDB).
99
+ - [ ] **Thin results writer** for sgnl (~50 lines, HTTP line-protocol POST to
100
+ VM). Lives in sgnl or a small dedicated package — not sgnmon.
101
+ - [ ] **sgn-llai migration, metrics first**: `MetricsCollectorMixin` →
102
+ `sgnmon.MetricsMixin`; `MetricsPipeline` → plain `Pipeline` +
103
+ `monitor.tap()`. DONE 2026-07-23: `DelayBuffer` +
104
+ `RoundRobinDistributor` moved into `sgnllai/transforms/` with metrics
105
+ ported to `sgnmon.MetricsMixin` — Prometheus-shaped families
106
+ (`delay_buffer_events_total{state=buffered|released|flushed_eos}`,
107
+ `delay_buffer_size`, `events_distributed_total{worker=N}`), skig's
108
+ auto-elapsed timing dropped (superseded by `sgnmon_exec_seconds`
109
+ taps); `metrics_enabled` kwarg kept for bin compatibility, optional
110
+ `monitor=` for test isolation; sgnmon added to sgn-llai deps; bins'
111
+ imports updated (EventLatency etc. still from sgneskig until the
112
+ metrics migration); 29 ported tests + full suite green (test_make_dag
113
+ collection error is a pre-existing missing `ezdag` dep), flake8/isort
114
+ clean, all uncommitted. With this, EVERY skig element needing a home
115
+ has one; what remains of skig is superseded (metrics layer,
116
+ MetricsPipeline, EventLatency) or dies with the Influx retirement
117
+ (writer, ScaldMetricsSink, influxdb/grafana modules, Grafana exporter,
118
+ all five bin scripts). Remaining here: swap sgn-llai's own
119
+ MetricsCollectorMixin/EventLatency/MetricsPipeline usage to sgnmon. Gotchas: sgnmon `EventLatency` defaults
120
+ `time_field="time"` (skig: `"gpstime"`) — pass explicitly; push/flusher
121
+ model becomes scrape — each service needs a `MonitorServer` port and a
122
+ Prometheus scrape-config entry; GPS point timestamps and
123
+ `storage_aggregate` have no equivalent — affected dashboards get rebuilt
124
+ against PromQL.
125
+ - [ ] **Ops wiring**: scrape configs for pipeline services on IGWN Prometheus;
126
+ `remote_write` → VM; `check_http` `/health` checks in IGWN monitoring.
127
+ - [x] **Transport element move** — COMMITTED 2026-07-23 on feature branches:
128
+ sgn-ligo `kafka-transport` (`fd94c82` unified KafkaSink w/ provenance
129
+ + derivation from Yun-Jing Huang's implementation in the message,
130
+ `35e921b` KafkaSource + json_frames), sgn-llai `skig-elements`
131
+ (`8cc4d8c` DelayBuffer/RoundRobinDistributor on sgnmon metrics,
132
+ `a7a1719` IGWNAlertSource), sgnl `kafka-sink-args` (`8c9ea6f` sink
133
+ kwargs). Module docstrings carry no port notes per Olivia — commit
134
+ messages hold provenance. Not pushed; MRs when Olivia is ready.
135
+ Original notes: Done: merged `KafkaSource` (auth="none|scitoken|auto", helpers +
136
+ lazy igwn-auth-utils) in `sgnligo/sources/kafka_source.py`;
137
+ `IGWNAlertSource` and `json_frames.py` copied; unified `KafkaSink`
138
+ (payload_format="json"|"scald", stdout dry-run mode, LIGOTimeGPS-safe
139
+ encoding on all paths) replacing the scald-client sink; three deps added
140
+ to sgn-ligo pyproject; sgnl call sites (`inspiral.py`, `ll_dq.py`)
141
+ updated to `bootstrap_servers=` + `payload_format="scald"`; smoke tests
142
+ pass. Note: sgn-ligo checkout moved from `compat` branch to `main`
143
+ (compat's sgnts.compat layer isn't in the installed sgnts).
144
+ 2026-07-23: test port complete — tests/test_kafka_source.py (71),
145
+ test_kafka_sink.py (39, incl. new scald-mode coverage: accumulation,
146
+ trigger flush, interval batching, LIGOTimeGPS, stdout mode),
147
+ test_igwn_alert_source.py (13), test_json_frames.py (ported, minus
148
+ skig's version-fallback test); sgn-ligo's OWN bins also migrated
149
+ (`sgnligo/bin/ll_dq.py`, `gwistat.py` used the old sink kwargs — found
150
+ via test failure); one pre-existing test assertion updated
151
+ (test_ll_dq test_no_kafka_server, asserts new kwargs). mypy + flake8
152
+ clean, 182 tests green across affected files, full suite passes (gwosc
153
+ live-data tests are network-flaky, unrelated). All uncommitted.
154
+ Remaining: sgn-llai import updates (deferred to the metrics-first
155
+ sgn-llai migration), commit/MR when Olivia is ready.
156
+ COMMIT-MESSAGE NOTE (Olivia, 2026-07-23): module docstrings carry no
157
+ port/provenance notes — the sgn-ligo commit message must say: ported
158
+ from sgn-skig; KafkaSource merges sgneskig's KafkaSource +
159
+ ScitokenKafkaSource (auth folded into an option); KafkaSink unifies
160
+ sgnligo's scald-format sink (scald wire format now produced directly,
161
+ dropping ligo.scald.io.kafka) with sgneskig's generic JSON sink;
162
+ IGWNAlertSource and json_frames ported as-is.
163
+ - [ ] **Archive sgn-skig** with a README pointer — only after the migration is
164
+ *deployed*, not merely merged.
165
+
166
+ ## Follow-ups (no urgency)
167
+
168
+ - [ ] Upstream conversation with sgn maintainers: sgnmon consuming sgn's
169
+ built-in `record_frame`/`record_exec` hooks instead of wrapping
170
+ `pad.call` — would remove the third parallel interception mechanism
171
+ (CONSOLIDATION.md §3).
172
+ - [x] Document the exec-metric coverage limitation in the sgnmon user docs —
173
+ done 2026-07-23 (metrics.md + attaching.md: `src`/`snk` taps only, not
174
+ `adp` wraps or in-graph elements; absence means "not measured").
175
+ - [ ] `sgnligo/sinks/influx_sink.py` (scald→Influx, separate from skig):
176
+ grep across sgnl/sgn-cal/sgn-llai/sgn-ligo/gstlal found **zero
177
+ consumers** (only the `sgnligo.sinks` re-export) — likely deletable
178
+ rather than migratable. Confirm no external/site users, then remove;
179
+ that clears the last scald→Influx writer outside skig without needing
180
+ the VM push path for it.
@@ -0,0 +1,273 @@
1
+ # Monitoring consolidation: sgnmon, sgn-skig, and sgn testpoints
2
+
3
+ Comparative analysis of the three monitoring systems in the sgn ecosystem, with a
4
+ consolidation plan to deprecate sgn-skig and rally behind sgnmon as the monitoring
5
+ library. Written 2026-07-22.
6
+
7
+ ## TL;DR
8
+
9
+ The three systems barely overlap in what they *collect* — they overlap in *mission*.
10
+ sgnmon is already the strongest general-purpose monitoring library of the three: it
11
+ collects far more out of the box, has health checks, a live dashboard, and a
12
+ Prometheus-native model. sgn-skig's real value is not its metrics (it ships almost
13
+ none) but its **custom-metrics ergonomics** and its **InfluxDB/scald/Grafana ops
14
+ story**; both have clean Prometheus-world equivalents. sgn's testpoints are a
15
+ terminal debugging tool, not a monitoring system — they stay, they're complementary.
16
+ The one hard prerequisite for deprecating sgn-skig: **sgn-llai is its only downstream
17
+ consumer**, and it uses everything — including the Kafka transport elements, which
18
+ are not monitoring code and need a new home before sgn-skig can be archived.
19
+
20
+ ## 1. What each system is
21
+
22
+ **sgnmon** observes frames at pad granularity via two attachment styles —
23
+ non-invasive taps that wrap pad callables (including post-audioadapter taps), and
24
+ in-graph `MonitorTransform`/`MonitorSink` elements. It exposes ten Prometheus metric
25
+ families plus windowed gauges over an in-memory two-resolution bucket store (1 s
26
+ buckets for windows up to 300 s, 60 s buckets up to 6 h), six health checks (three on
27
+ by default, self-calibrating), a Starlette server with `/metrics`, `/health`,
28
+ `/status`, `/graph`, and `/events` SSE, a live SVG dashboard, and a Typer CLI with
29
+ zero-code attach (`sgnmon run -- <cmd>`) and a Nagios-style `sgnmon check`.
30
+
31
+ **sgn-skig** (Scald–Kafka–InfluxDB–Grafana) is a metrics *pipeline to storage*, not a
32
+ metrics *collector*. Elements mix in `MetricsCollectorMixin`, declare
33
+ `MetricDeclaration` schemas (timing/counter/gauge + tags), and record via
34
+ `time_operation()` / `increment_counter()` / `record_metric()`. A shared
35
+ background-flushing `MetricsWriter` pushes to InfluxDB through ligo.scald, which owns
36
+ the powers-of-10 reduction. A `GrafanaExporter` generates dashboards from the
37
+ declared schemas, and a small Flask "ginterval" service picks the right retention
38
+ policy for a Grafana time range. It also carries transport elements (KafkaSource,
39
+ KafkaSink, ScitokenKafkaSource, IGWNAlertSource) and utility transforms
40
+ (DelayBuffer, RoundRobinDistributor, EventLatency) that are unrelated to monitoring.
41
+
42
+ **sgn testpoints** (`src/sgn/testpoint.py` in sgn) are an env-var-activated
43
+ (`TESTPOINT=frame|exec`), in-process debugging facility that renders a live `rich`
44
+ terminal table. Frame mode keeps only the *last* frame per pad; exec mode keeps a
45
+ 10-sample sliding window of per-pad execution time (mean/stdev). No HTTP, no export,
46
+ no retention. Its hooks are compiled into `SourcePad.__call__`/`SinkPad.__call__` and
47
+ the graph executor, so it sees things no external library can: actual frame objects
48
+ at every pad boundary and true per-node compute time inside the scheduler.
49
+ Separately, sgn also ships `StatsSource` (psutil process/system metrics emitted *as
50
+ frames*, i.e. data-plane), memory profiling, graphviz pipeline visualization, and a
51
+ bottle-based HTTP control plane.
52
+
53
+ ## 2. Metrics collected — side by side
54
+
55
+ | What | sgnmon | sgn-skig | sgn testpoints |
56
+ |---|---|---|---|
57
+ | Frame/throughput counts | ✅ `frames`, `gap_frames`, `data/gap_seconds`, `samples` per pad | ❌ | ❌ (last frame snapshot only) |
58
+ | Data latency (GPS now − frame end) | ✅ histogram + last-value gauge per pad, plus `EventLatency` for event streams | ⚠️ only via the `EventLatency` element you must splice in | ❌ |
59
+ | Per-element/pad **execution time** | ✅ `sgnmon_exec_seconds` histogram from `src`/`snk` taps | ✅ auto elapsed-time per pull→new cycle | ✅ exec mode, 10-sample window, terminal only |
60
+ | Gap/liveness/EOS | ✅ gap ratio windows, EOS gauge, freshness timestamps | ❌ | ⚠️ `has gap` column of last frame |
61
+ | Process/system stats (CPU, RSS, IO) | ✅ opt-in `Monitor(process_metrics=True)` (Linux) | ❌ | ⚠️ `StatsSource` (as pipeline frames, not monitoring) |
62
+ | App-defined custom metrics | ✅ declarative `MetricSpec`/`MetricsMixin` + `Monitor.counter/gauge/histogram` | ✅ declarative schema + recording API | ❌ |
63
+ | Windowed rollups in-process | ✅ 5 s–6 h trailing windows | ❌ (delegated to scald/Influx) | ⚠️ 10-sample exec deque |
64
+ | Health checks | ✅ 6 checks, adaptive baselines, HTTP 503 semantics | ❌ | ❌ |
65
+
66
+ The striking asymmetry: sgn-skig ships only three metric-emitting transforms
67
+ (`events_buffered/released/flushed_eos`, `buffer_size`, `events_distributed`,
68
+ `event_latency`) plus the automatic per-cycle elapsed timer. Everything sgnmon
69
+ measures out of the box, sgn-skig simply doesn't measure. The two things sgnmon
70
+ originally did **not** measure — per-pad execution time and process resource
71
+ usage — existed only in the other two projects; both landed in sgnmon on
72
+ 2026-07-22 (§6).
73
+
74
+ ## 3. Feature overlap map
75
+
76
+ Genuine overlap across all three is narrow:
77
+
78
+ - **Pipeline topology visualization exists three times**: sgnmon's live SVG dashboard
79
+ + `/graph` JSON, sgn-skig's Mermaid panel in generated Grafana dashboards, and
80
+ sgn's graphviz `Pipeline.visualize()`. sgnmon's `/graph` is the only
81
+ machine-readable, live one; the Mermaid generator can die with sgn-skig, and
82
+ graphviz stays as a static docs/debug tool.
83
+ - **Per-pad observation hooks exist three times**: sgnmon taps wrap `pad.call`;
84
+ testpoints have hooks compiled into sgn's `base.py`/`apps.py`; sgn-skig
85
+ monkey-wraps `pull()`/`new()` via `__init_subclass__`. Three interception
86
+ mechanisms doing the same job at three code layers. Longer-term, sgn's built-in
87
+ `record_frame`/`record_exec` hook points are the architecturally cleanest
88
+ interception layer — sgnmon could optionally consume them instead of wrapping pads
89
+ itself, but that is an optimization, not a blocker.
90
+ - **Timing**: sgn-skig's elapsed-time auto-tracking ≈ testpoints' exec mode ≈ nothing
91
+ in sgnmon. This is the one real *metric* overlap between skig and testpoints, and
92
+ the one gap in sgnmon.
93
+
94
+ Everything else is disjoint: health checks, SSE dashboard, and Prometheus exposition
95
+ exist only in sgnmon; durable storage, Grafana generation, and multi-resolution
96
+ retention exist only in sgn-skig; frame-content inspection exists only in testpoints.
97
+
98
+ ## 4. Custom metrics in sgnmon
99
+
100
+ `Monitor` already owns a standard `prometheus_client.CollectorRegistry`, so custom
101
+ metrics are already *possible* — just undocumented and unergonomic. What is worth
102
+ porting from sgn-skig is the ergonomics, not the machinery:
103
+
104
+ 1. **A small declarative layer on `Monitor`**: `monitor.counter(name, description,
105
+ labels)`, `monitor.gauge(...)`, `monitor.histogram(...)` returning
106
+ prometheus_client families registered in the monitor's registry.
107
+ 2. **A mixin for custom elements** (the `MetricsCollectorMixin` replacement)
108
+ providing sgn-skig's recording verbs backed by those families:
109
+ `increment_counter(name, amount, tags)`, `set_gauge(...)`, and a
110
+ `time_operation(name, tags)` context manager backed by a Histogram. Keeping the
111
+ verb names close to sgn-skig's makes the sgn-llai migration mostly find-and-replace.
112
+ 3. **Type mapping**: skig `counter` → Counter (its `storage_aggregate: sum` concern
113
+ evaporates — Prometheus counters are cumulative, `increase()` is exact at any
114
+ resolution); skig `timing` → Histogram, optionally with a last-value gauge
115
+ (histograms preserve tail behavior at any query resolution, which is what skig's
116
+ `max` aggregate was hand-rolling); skig `gauge` → Gauge.
117
+
118
+ Deliberately not carried over: `MetricsPipeline` (tapping means no Pipeline subclass
119
+ is needed), the writer/flusher thread (scraping replaces pushing), schema
120
+ auto-registration against Influx, and `storage_aggregate`.
121
+
122
+ ## 5. Powers-of-10 aggregation — what it solves, and the alternatives
123
+
124
+ What the scald scheme is *for*: bounded query cost (~300–1000 points) over any
125
+ Grafana time range, achieved by write-side reduction every 300 s into six InfluxDB
126
+ retention policies (1 s → 100 000 s), with per-type aggregate functions (sum for
127
+ counters so counts don't inflate, max for timings so spikes survive), plus
128
+ sgn-skig's Flask ginterval service doing read-side resolution selection via a hidden
129
+ Grafana template variable.
130
+
131
+ In the Prometheus model sgnmon commits to, **most of that problem does not exist**,
132
+ and the rest is an ops decision outside the library:
133
+
134
+ - **Correctness needs no pre-aggregation.** Counters are cumulative, so
135
+ `rate()`/`increase()` are exact at any step size; Grafana auto-scales the query
136
+ step with the time range, so point counts are bounded by construction. Latency
137
+ tails survive via the histogram (quantiles compose over any range) and
138
+ `max_over_time` on the gauges. Both aggregate-correctness rationales documented in
139
+ skig's `concepts.md` are native PromQL semantics.
140
+ - **What remains is long-range query cost and retention** — a TSDB deployment
141
+ choice, in rough order of preference:
142
+ 1. **Plain Prometheus with long retention.** sgnmon's cardinality is tiny (10
143
+ families × element × pad × a few jobs). A single Prometheus with 1–2 y
144
+ retention handles this scale trivially; query-time stepping does the
145
+ "resolution selection" for free. Start here; escalate only if range queries
146
+ actually get slow.
147
+ 2. **Recording rules** for the handful of expensive range queries (e.g. 5 m
148
+ rollups of `rate(sgnmon_data_seconds[...])`). The moral equivalent of scald's
149
+ write-side reduction, expressed in ~20 lines of ops YAML instead of a library
150
+ feature plus a bespoke Flask service.
151
+ 3. **Thanos**, if years of history in object storage are needed: its compactor
152
+ downsamples to 5 m and 1 h resolutions and its query layer auto-selects
153
+ resolution per time range — the maintained-infrastructure version of
154
+ powers-of-10 + ginterval. Mimir/Cortex are the scale-out alternatives but do
155
+ *not* downsample; VictoriaMetrics is very cheap for long raw retention but
156
+ gates downsampling behind its enterprise edition.
157
+ 4. **Bridge option** if ops is institutionally committed to InfluxDB/Grafana:
158
+ scrape `/metrics` with Telegraf into InfluxDB and keep continuous queries
159
+ there. Retains the storage stack with zero sgn-skig code.
160
+ - **The genuinely open ops question is push vs. pull**, not aggregation: sgn-skig
161
+ pushes, which works from ephemeral Condor jobs; Prometheus pulls, which requires
162
+ the scraper to find the job. Standard answers: a Grafana Alloy/vmagent sidecar
163
+ that scrapes localhost and `remote_write`s out, or file-based service discovery
164
+ fed from job submission. This is the one item to settle with ops *before*
165
+ declaring sgn-skig deprecated — the only thing skig's architecture handles that
166
+ sgnmon does not yet have a designated answer for.
167
+
168
+ ## 6. Gaps sgnmon should close — CLOSED 2026-07-22
169
+
170
+ 1. **Per-pad execution time** — DONE (`0b61ebe`): taps time the original pad
171
+ callable into the `sgnmon_exec_seconds` histogram (buckets 100µs–5s,
172
+ `Monitor(exec_buckets=...)`). Known limitation: recorded on `src`/`snk` taps
173
+ only — not on `adp` internal-pad wraps (one wrap serves several probes) and
174
+ not by the in-graph elements — so absence of exec data for those probes means
175
+ "not measured", not "zero cost".
176
+ 2. **Process metrics** — DONE (`f97881e`): `Monitor(process_metrics=True)` /
177
+ `sgnmon run --process-metrics` registers the prometheus_client process
178
+ collector (`/proc`-based, Linux only). sgn's `StatsSource` keeps its
179
+ data-plane role.
180
+ 3. **Custom metrics API** — DONE (`048f98e`): `sgnmon.metrics` with
181
+ `MetricSpec`/`MetricsMixin` (skig-compatible verbs) plus
182
+ `Monitor.counter/gauge/histogram` factories.
183
+ 4. **Event-frame latency** — DONE (`dbb144e`): `EventLatency` element records
184
+ per-event GPS latency through the new `PadProbe.record_latency` without
185
+ inflating frame counts. Note the default `time_field` is `"time"` (skig
186
+ defaulted to `"gpstime"`) — migrations must pass it explicitly.
187
+
188
+ ## 7. Deprecation path for sgn-skig
189
+
190
+ Exactly one downstream consumer exists: **sgn-llai**, and it imports the full
191
+ surface — `MetricsCollectorMixin`/`MetricDeclaration` (heavily, for custom metrics),
192
+ `MetricsPipeline`, `EventLatency`, `DelayBuffer`, `RoundRobinDistributor`, and
193
+ `KafkaSource`/`KafkaSink`/`ScitokenKafkaSource`. The plan splits in three:
194
+
195
+ - **Monitoring surface → sgnmon** (§4/§6). After that, sgn-llai's `MetricsPipeline`
196
+ becomes plain `Pipeline` + `monitor.tap()`, and its mixin usage becomes the sgnmon
197
+ mixin.
198
+ - **Transport elements → a new home.** KafkaSource/KafkaSink/ScitokenKafkaSource/
199
+ IGWNAlertSource are ingestion, not monitoring — they cannot land in sgnmon without
200
+ recreating the scope creep that made skig awkward. Options: a new `sgn-kafka`
201
+ package, or fold into sgn-ligo; the SciToken/IGWN-alert pieces argue for the
202
+ LIGO-flavored home. `DelayBuffer`/`RoundRobinDistributor` go with them or move
203
+ into sgn-llai itself if no one else needs them.
204
+ - **Ops surface → dropped or replaced by config.** Grafana dashboard generation
205
+ becomes largely unnecessary: because sgnmon's families and labels are uniform
206
+ across every pipeline, one static, hand-maintained Grafana dashboard with
207
+ `element`/`pad`/`job` variables serves all deployments — strictly better than
208
+ per-pipeline generated JSON. The ginterval service, provisioning CLIs, and
209
+ netrc/Influx user tooling are replaced by whatever TSDB choice comes out of §5.
210
+
211
+ Sequencing: (1) land §6 items in sgnmon — DONE 2026-07-22; (2) settle the
212
+ push-vs-pull + TSDB question with ops — decided, see §8; (3) migrate sgn-llai
213
+ (metrics first, transports once they have a home); (4) archive sgn-skig with a
214
+ README pointer.
215
+
216
+ Two items discovered after the original analysis, tracked in
217
+ [CONSOLIDATION-TRACKING.md](CONSOLIDATION-TRACKING.md) with the rest of the open
218
+ work: whether sgn-llai's CI depends on skig's `write_metrics_manifest()` YAML
219
+ export (dropped from the port — confirm before migrating), and health-check
220
+ integration with IGWN monitoring (resolved: a stock Nagios `check_http` against
221
+ `/health` suffices; no igwn-monitoring-plugins packaging needed).
222
+
223
+ **Testpoints**: unchanged. They are a zero-config terminal debugger with access to
224
+ live frame *contents* — a different job than fleet monitoring, and the only
225
+ user-facing thing sgnmon could subsume (exec timing) is §6 item 1.
226
+
227
+ ## 8. Storage decision (resolved 2026-07-22)
228
+
229
+ Deployment facts: production pipelines are long-lived services; IGWN ops already
230
+ runs Grafana and Prometheus; InfluxDB is maintained only for the sgn-skig stack.
231
+ Two workloads must be covered: operational metrics (weeks-long queries) and sgnl's
232
+ scientific results (GPS-stamped, sparse/backfilled series currently stored via
233
+ scald→InfluxDB and browsed in Grafana).
234
+
235
+ **Decision: Prometheus stays the only query layer; a single-node VictoriaMetrics
236
+ instance becomes the long-term backend; InfluxDB is retired.**
237
+
238
+ - Ops metrics: IGWN Prometheus scrapes each service's `/metrics` and
239
+ `remote_write`s to VictoriaMetrics for multi-year raw retention. No
240
+ downsampling needed at sgnmon's cardinality; recording rules are the escape
241
+ hatch if a specific range query ever gets slow.
242
+ - How this preserves scald's bounded-query property without pre-aggregation:
243
+ Grafana range queries carry a step ≈ range / max-data-points (~1000), so any
244
+ panel returns ~1000 points per series regardless of time span — the read-side
245
+ resolution selection ginterval provided, but automatic and per zoom level
246
+ (and zooming in reaches raw data, which scald expired after minutes). Scald's
247
+ write-time aggregate choice becomes a query-time choice via
248
+ `increase(x[$__interval])` / `max_over_time(x[$__interval])` etc. Server-side
249
+ scan cost of raw data is negligible at this scale (an 8-week, 20-series panel
250
+ scans ~10M samples; VM processes tens of millions per second per core), and
251
+ raw retention is single-digit GB/year. Caveat: sparse science series should
252
+ be queried with range functions or points panels — plain gauge queries only
253
+ fill forward within the ~5m staleness window; verify during the prototype.
254
+ - Scientific results: plain Prometheus scraping is the wrong tool (wall-clock
255
+ scrape timestamps, no backfill, no per-event fidelity). VictoriaMetrics accepts
256
+ pushed data with arbitrary timestamps via the InfluxDB v1 line-protocol `/write`
257
+ API and a timestamped Prometheus-text import endpoint, and serves the
258
+ Prometheus query API, so it appears in Grafana as just another Prometheus
259
+ datasource. sgnl pushes results directly; a thin line-protocol writer (~50
260
+ lines) should live in sgnl or a small dedicated package — not in sgnmon.
261
+ - "Current state" science values (latest range, live thresholds) can instead be
262
+ sgnmon custom gauges and ride the scrape path for free.
263
+ - Thanos was rejected (object-storage retention only, no push/backfill story —
264
+ would still require a second system for science data); Mimir likewise, plus
265
+ heavier operations.
266
+ - Migration checks: (a) prototype one representative sgnl series end to end
267
+ (line-protocol push of a few weeks of GPS-stamped values into a scratch VM
268
+ instance, rebuild the Grafana panel, compare against the Influx original);
269
+ (b) confirm the GPS-vs-Unix timestamp convention the existing scald dashboards
270
+ assume — VM/Grafana sample timestamps must be Unix time, with GPS kept as the
271
+ value or a companion series; (c) audit whether anything in sgnl's Influx data
272
+ is event *records* rather than time series — those belong in a proper service
273
+ (e.g. GraceDB), not any TSDB.