Newer Older
113 lines | 5.781kb
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