|
Bogdan Timofte
authored
2 weeks ago
|
1
|
# Telemetry Measurements Schema
|
|
|
2
|
|
|
|
3
|
The first production schema is now a single canonical file: `schema/telemetry_schema.sql`.
|
|
|
4
|
|
|
|
5
|
Legacy compatibility migrations were removed because this database has no prior production deployments. The schema file represents the current design directly and runs on an empty PostgreSQL database.
|
|
|
6
|
|
|
|
7
|
Source worker ingestion is documented in `docs/mqtt_ingestion_api.md`, with MQTT used there as a concrete example.
|
|
|
8
|
|
|
|
9
|
A draft note for future counter-style telemetry, including energy and traffic counters, is in `docs/energy_counter_draft.md`.
|
|
|
10
|
|
|
|
11
|
## Design note
|
|
|
12
|
|
|
|
13
|
### Generic segment tables
|
|
|
14
|
|
|
|
15
|
Segment tables are now created through:
|
|
|
16
|
|
|
|
17
|
- `telemetry.create_segment_table(p_table_name, p_value_type)`
|
|
|
18
|
|
|
|
19
|
Supported value types are:
|
|
|
20
|
|
|
|
21
|
- `double precision`
|
|
|
22
|
- `boolean`
|
|
|
23
|
- `smallint`
|
|
|
24
|
|
|
|
25
|
The generated tables keep the existing historian shape and protections:
|
|
|
26
|
|
|
|
27
|
- one open segment per device
|
|
|
28
|
- internal `device_pk` foreign key for future numeric-key joins while keeping `device_id` for compatibility
|
|
|
29
|
- `policy_id` foreign key to `telemetry.metric_policies(policy_id)` so every stored interval remembers which ingestion policy produced it
|
|
|
30
|
- foreign key to `telemetry.devices(device_id)` with `ON UPDATE/DELETE CASCADE`
|
|
|
31
|
- non-overlapping `tstzrange` periods enforced with `EXCLUDE USING gist`
|
|
|
32
|
- covering index on `(device_id, start_time DESC)` for point/range lookups
|
|
|
33
|
- additional GiST index on `(device_id, segment_period)` for range sampling/overlap queries
|
|
|
34
|
- storage tuning via `fillfactor` and aggressive autovacuum/analyze thresholds
|
|
|
35
|
|
|
|
36
|
### Generic ingestion engine
|
|
|
37
|
|
|
|
38
|
Runtime ingestion now flows through a single function:
|
|
|
39
|
|
|
|
40
|
- `telemetry.ingest_segment(...)`
|
|
|
41
|
|
|
|
42
|
`telemetry.metrics.table_name` is now stored as plain `text`, not `regclass`. Runtime code normalizes it, validates that it resolves under the `telemetry` schema, and only then interpolates the quoted identifier into dynamic SQL.
|
|
|
43
|
|
|
|
44
|
Dynamic SQL is used only to parameterize the target table name. All values stay bound through `USING`, so the only dynamic part of the hot path is table access.
|
|
|
45
|
|
|
|
46
|
The generic engine preserves the existing historian semantics:
|
|
|
47
|
|
|
|
48
|
- append-only watermark enforcement
|
|
|
49
|
- advisory locking per `(metric, device)`
|
|
|
50
|
- automatic device provisioning into `telemetry.devices`
|
|
|
51
|
- `last_seen` updates on successful ingestion
|
|
|
52
|
- epsilon/exact consolidation depending on policy
|
|
|
53
|
- lazy gap detection
|
|
|
54
|
- explicit `NULL` segments
|
|
|
55
|
- query-time synthetic `NULL` tails
|
|
|
56
|
|
|
|
57
|
### Device registry
|
|
|
58
|
|
|
|
59
|
`telemetry.devices` is the canonical device catalog. Ingestion still accepts unseen devices without a separate provisioning step, but it inserts them into the registry before segment writes so foreign keys remain valid.
|
|
|
60
|
|
|
|
61
|
Deleting or renaming a device is now referentially safe because segment tables and watermark rows cascade from the device registry.
|
|
|
62
|
|
|
|
63
|
`telemetry.devices` also has an internal `device_pk` surrogate primary key. Segment tables keep `device_id` for API compatibility, but now store `device_pk` as the internal foreign key.
|
|
|
64
|
|
|
|
65
|
### Metric registry
|
|
|
66
|
|
|
|
67
|
`telemetry.metrics` keeps `metric_name` as the external API identifier, but now uses `metric_pk` as the internal primary key. Text identifiers remain unique and stable for backward compatibility.
|
|
|
68
|
|
|
|
69
|
### Metric policy
|
|
|
70
|
|
|
|
71
|
`telemetry.metrics` now carries both:
|
|
|
72
|
|
|
|
73
|
- `metric_type`
|
|
|
74
|
- `comparison_mode`
|
|
|
75
|
|
|
|
76
|
`metric_type` and `comparison_mode` are PostgreSQL enums, so invalid values fail with type-level errors and show up clearly in schema introspection.
|
|
|
77
|
|
|
|
78
|
Current combinations are:
|
|
|
79
|
|
|
|
80
|
- numeric -> `comparison_mode = 'epsilon'`
|
|
|
81
|
- boolean -> `comparison_mode = 'exact'`
|
|
|
82
|
|
|
|
83
|
Runtime ingestion policy is now versioned in `telemetry.metric_policies`. The active row is selected by `(metric_name, valid_from <= observed_at)` with the greatest `valid_from`, and new segments store that `policy_id`. Existing metrics are seeded with an initial `valid_from = '-infinity'` policy row so historical segments stay compatible.
|
|
|
84
|
|
|
|
85
|
This keeps historical semantics stable when policy parameters such as `epsilon`, `rounding_precision`, `allow_null`, or `max_sampling_interval` change over time. Query wrappers do not need to re-resolve policy history; they use the `policy_id` already attached to each segment when they need policy-specific behavior such as open-segment tail cutoff.
|
|
|
86
|
|
|
|
87
|
New policies are added through `telemetry.add_metric_policy(...)`. `telemetry.register_numeric_metric(...)` and `telemetry.register_boolean_metric(...)` still provision the metric and ensure the initial `-infinity` policy exists.
|
|
|
88
|
|
|
|
89
|
If a policy changes while a segment is still open, ingestion now splits that segment at `policy.valid_from` before handling the incoming sample. The synthetic continuation segment starts exactly at the boundary with `samples_count = 0`, so no interval can silently extend past the policy that generated it.
|
|
|
90
|
|
|
|
91
|
The schema also allows a future `state` metric type backed by `smallint` with `comparison_mode = 'exact'`, without changing the segment engine again.
|
|
|
92
|
|
|
|
93
|
### Query API
|
|
|
94
|
|
|
|
95
|
The internal table readers are generic:
|
|
|
96
|
|
|
|
97
|
- `telemetry.value_at_from_table(...)`
|
|
|
98
|
- `telemetry.segments_between_from_table(...)`
|
|
|
99
|
- `telemetry.samples_from_table(...)`
|
|
|
100
|
|
|
|
101
|
Public wrappers stay typed because PostgreSQL function return types are static:
|
|
|
102
|
|
|
|
103
|
- numeric: `telemetry.value_at(...)`, `telemetry.metric_segments(...)`, `telemetry.sample_metric(...)`
|
|
|
104
|
- boolean: `telemetry.boolean_value_at(...)`, `telemetry.boolean_segments_between(...)`, `telemetry.boolean_samples(...)`
|
|
|
105
|
|
|
|
106
|
### Maintenance
|
|
|
107
|
|
|
|
108
|
The schema now also includes:
|
|
|
109
|
|
|
|
110
|
- `telemetry.verify_segments(...)` for integrity checks
|
|
|
111
|
- `telemetry.compact_segments(...)` for manual zero-gap identical-value compaction
|
|
|
112
|
- `telemetry.metric_retention_policies` as a retention/compaction policy registry
|
|
|
113
|
- `telemetry.inactive_devices(...)` for offline-device detection
|