docs: publish Preview 19 dogfood evidence
Export the reviewed allowlisted snapshot from private source commit 05928cebd01b586cf9e9d4b8c8537a7605a6068c. This records the exact candidate, bounded capacity result, stateful migration scratch requirement, authenticated batch identity proof, and immediate live acceptance evidence. AI-Assisted: OpenAI Codex Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Adaptive time-partitioned projections
|
||||
|
||||
Status: design candidate; not an implemented capability or release claim.
|
||||
|
||||
## Purpose
|
||||
|
||||
Observatory should preserve forensic evidence by default without requiring
|
||||
every accepted observation to become a fully indexed row immediately. The raw
|
||||
segment and control catalogue already contain the durable identity, tenant,
|
||||
signal, source, sequence, byte size, and observed-time range needed to defer
|
||||
most read-model work.
|
||||
|
||||
The proposed model is an event lake with adaptive materializations:
|
||||
|
||||
1. acknowledge immutable raw evidence and its small catalogue entry;
|
||||
2. keep a bounded recent window available for live authorized views;
|
||||
3. incrementally maintain only projections required by currently observed
|
||||
views, explicitly warmed workloads, or reviewed central correlations;
|
||||
4. build ad-hoc historical materializations on demand from the relevant raw
|
||||
time partitions; and
|
||||
5. evict any derived projection that no longer justifies its storage cost.
|
||||
|
||||
Raw evidence remains authoritative. Every memory view, SQLite partition,
|
||||
index, rollup, and query cache remains disposable and reconstructable.
|
||||
|
||||
## Batch identity, not per-record deduplication
|
||||
|
||||
An enrolled agent is trusted to classify and serialize the records it was
|
||||
configured to collect. Observatory does not need to maintain a global
|
||||
per-record deduplication index in the ingestion path. The durable unit is one
|
||||
bounded authenticated batch identified by its enrolled source, stream, and
|
||||
monotonic sequence. Its retained segment catalogue records the first and last
|
||||
observed timestamps, record and byte counts, and the exact segment and
|
||||
canonical logical-batch digests. Agent epochs separately namespace locally
|
||||
generated alert-transition sequences across process-state replacement.
|
||||
|
||||
The server derives organization, project, environment, and service scope from
|
||||
the enrollment. It commits the immutable batch once, advances the stream
|
||||
watermark atomically, and acknowledges its exact identity. Retrying the same
|
||||
sequence and digest is an idempotent no-op. Reusing a sequence with different
|
||||
bytes is quarantined as corruption. A sequence gap is retained as an explicit
|
||||
continuity event rather than hidden. Timestamp ranges select storage and query
|
||||
partitions but are not deduplication keys because legitimate batches may
|
||||
overlap and host clocks may drift.
|
||||
|
||||
### Implemented native v2 early-replay envelope
|
||||
|
||||
The agent sends new native batches to `/api/v2/ingest/native`. A small
|
||||
authenticated header envelope is available before the JSON record body, so an
|
||||
exact replay takes a cheaper path. The envelope contains only:
|
||||
|
||||
- protocol version, stream ID, signal, and monotonic sequence;
|
||||
- the SHA-256 of the exact encoded body and the canonical logical-batch digest;
|
||||
- record count and encoded byte count; and
|
||||
- first and last observed timestamps.
|
||||
|
||||
Source and tenant scope remain server-derived from the enrolled credential.
|
||||
For a sequence already acknowledged with both digests, the server hashes and
|
||||
discards the bounded body, returns the retained raw-segment identity, and avoids
|
||||
JSON decoding, compression, catalogue writes, or projection wakeups. It must
|
||||
still read the complete bounded body and verify its encoded digest before
|
||||
acknowledging; trusting an enrolled agent does not make a buggy reused sequence
|
||||
safe to accept without checking the bytes.
|
||||
|
||||
For a new sequence, the existing validation and raw-first commit remain
|
||||
mandatory. The server checks the declared count, byte size, signal, and time
|
||||
bounds against the decoded batch before it commits or acknowledges it. An
|
||||
envelope mismatch is a rejected batch, not a reason to rewrite telemetry.
|
||||
Overlapping time ranges, late data, and repeated values remain valid. This is
|
||||
an ingestion-cost optimization and audit index, not content-based record
|
||||
deduplication.
|
||||
|
||||
The legacy `/api/v1/ingest/native` endpoint remains available during the
|
||||
preview compatibility window. A stream last accepted through v1 has blank
|
||||
envelope columns. Its first matching v2 retry follows the full validation path
|
||||
once, verifies the retained segment identity, and backfills the v2 metadata;
|
||||
later exact retries use the early path.
|
||||
|
||||
## Partition shape
|
||||
|
||||
Partition first by organization, signal, and UTC time—not by arbitrary field
|
||||
names or user-supplied identifiers. A starting comparison should use one
|
||||
SQLite database per organization, signal, and UTC day. This keeps a normal
|
||||
30-day hot window near 90 files per organization, permits logs, metrics, and
|
||||
traces to project concurrently, narrows index working sets, and makes expiry a
|
||||
bounded file operation.
|
||||
|
||||
Hourly partitions may be evaluated for unusually high-volume days only after
|
||||
the daily prototype is measured. Five-minute, hourly, and daily *rollups* are
|
||||
aggregation resolutions; they should not be confused with the physical file
|
||||
boundary. The partition catalogue must tell the planner which exact files and
|
||||
rollup resolutions cover a requested window.
|
||||
|
||||
Each materialized partition records:
|
||||
|
||||
- organization, signal, UTC start and end;
|
||||
- projection schema and generation;
|
||||
- the exact raw segment digests or a durable high-watermark plus delta ledger;
|
||||
- row, byte, and rollup counts;
|
||||
- creation, last-query, and expiry times;
|
||||
- build state and failure state; and
|
||||
- the reviewed query evidence that justified eager or cached materialization.
|
||||
|
||||
Late observations update only their observed-time partition. The new
|
||||
generation is built beside the active one and atomically activated. Queries
|
||||
must either use one complete generation or merge its explicit delta; they may
|
||||
never silently omit late evidence.
|
||||
|
||||
## Materialization classes
|
||||
|
||||
### Live tail
|
||||
|
||||
A bounded per-organization memory ring receives records only after their raw
|
||||
segment has been durably acknowledged. It is keyed by segment digest and record
|
||||
index, capped by both age and bytes, and safe to lose. It serves the recent log
|
||||
tail, current metric samples, and trace arrivals while the disk projector is
|
||||
behind. Authorization is checked before the ring is opened, and disk results
|
||||
are deduplicated when a query overlaps the ring.
|
||||
|
||||
SSE is the default UI transport for these server-to-client updates. It works
|
||||
through ordinary HTTPS, has browser reconnection semantics, and is directly
|
||||
testable. WebSockets remain a future option for a feature that needs genuine
|
||||
bidirectional streaming; form mutations continue to use server-side POST,
|
||||
redirect, and flash-message flows.
|
||||
|
||||
### Agent-owned alert evaluation
|
||||
|
||||
Single-source alert rules should normally execute at the enrolled agent that
|
||||
already sees the stream. Rules are bounded, declarative, locally configured,
|
||||
and versioned. The server cannot remotely add a source path, collector, shell
|
||||
command, or executable rule. An agent evaluates its hot in-memory batch state
|
||||
without requiring the server to project that stream and emits only bounded
|
||||
rule-evaluation results: matched, clear, or an explicit evaluation error. The
|
||||
server remains the authority that applies consecutive-match policy and turns
|
||||
those evaluations into pending, firing, acknowledged, silenced, and resolved
|
||||
incident states.
|
||||
|
||||
Each transition is authenticated by its enrolled source and binds:
|
||||
|
||||
- rule identifier and version;
|
||||
- source, stream, and agent epoch;
|
||||
- evaluation window and state;
|
||||
- exact raw batch sequence range and segment digest evidence; and
|
||||
- a monotonic transition sequence and observed timestamp.
|
||||
|
||||
It contains no telemetry values by default. The server deduplicates the
|
||||
transition and binds it to the exact retained raw segment before accepting it.
|
||||
One agent cannot assert another source's scope. Reusing a sequence with
|
||||
different bytes is corruption, while replaying the same sequence and digest
|
||||
is an idempotent no-op. During the compatibility phase the server records this
|
||||
source evidence without mutating incident state; the existing central
|
||||
evaluator remains the oracle until differential comparisons prove equivalent
|
||||
results and failure behavior.
|
||||
|
||||
This model deliberately trusts an authenticated agent to classify and
|
||||
evaluate its own data. It still protects ordinary retries, crashes, replay,
|
||||
configuration drift, and a buggy agent. It cannot prove that a compromised or
|
||||
offline agent reported honestly. The server therefore retains source
|
||||
heartbeat, sequence-gap, and enrollment-revocation checks: a dead agent cannot
|
||||
report its own absence. Cross-source or cross-service correlation remains an
|
||||
explicit, bounded central workload rather than the default alert path.
|
||||
|
||||
The first implementation slice evaluates filter-only log rules over one exact
|
||||
durable batch. It deliberately excludes historical windows and cross-batch
|
||||
state while the source and central results are compared. A raw batch remains
|
||||
in the agent spool until both the raw admission and its source evaluation have
|
||||
matching acknowledgements. This gives the transition durable retry behavior
|
||||
without introducing a second telemetry store at the edge.
|
||||
|
||||
Incident evidence labels distinguish `source_reported`,
|
||||
`server_observed_absence`, and `centrally_correlated` conclusions. The current
|
||||
server-side saved-query evaluator remains the compatibility oracle until edge
|
||||
evaluation passes differential tests, disconnect/replay testing, and a
|
||||
production soak. Merely pinning or saving a dashboard never enables eager
|
||||
evaluation.
|
||||
|
||||
### Warm
|
||||
|
||||
Frequently used saved queries retain their materializations for a configured
|
||||
period. Query evidence may propose warming or eviction, but an administrator
|
||||
approves durable storage-policy changes.
|
||||
|
||||
### Observed leases
|
||||
|
||||
An authorized live Explore or dashboard session may create a bounded
|
||||
materialization lease for its exact organization, query, and time window.
|
||||
Every observer of the same materialization key shares one build/cache entry;
|
||||
50 or 1,000 viewers increase notification fan-out, not projection work. The
|
||||
SSE connection carries invalidation notices and renews demand; it does not
|
||||
carry telemetry and is not itself storage authority. Equivalent future
|
||||
WebSocket transport would obey the same lease contract.
|
||||
|
||||
The key includes the canonical typed query AST, authorized scope, bounded time
|
||||
window, descriptor generation, and materialization schema. A leased
|
||||
materialization may contain a query-specific temporary index, but Observatory
|
||||
does not create a permanent organization-wide index for every dashboard.
|
||||
Repeated measured benefit may produce a reviewed warm-index proposal with its
|
||||
estimated build, storage, and write-amplification cost. Approval and quota—not
|
||||
viewer count alone—promote it to durable policy.
|
||||
|
||||
Leases are deduplicated, expire after a disconnect grace period, and are
|
||||
limited per user and organization. Connection churn cannot create a new
|
||||
unbounded job, extend retention, or bypass query cost and sensitive-field
|
||||
permissions. A completed materialization may cool into the ordinary warm cache
|
||||
or be evicted. Agent-owned rules and server heartbeat checks remain
|
||||
independent of browser observers; a central cross-source rule acquires its own
|
||||
explicit, bounded lease.
|
||||
|
||||
### Lazy
|
||||
|
||||
An ad-hoc historical query first uses the segment catalogue to identify exact
|
||||
time and scope candidates. A small query may scan them directly under the
|
||||
ordinary time, byte, row, and memory budgets. A larger query creates a bounded
|
||||
materialization job. The UI reports its state and progress, then refreshes the
|
||||
result through SSE; the no-JavaScript path remains a normal status page with a
|
||||
manual refresh.
|
||||
|
||||
The first query is allowed to be slower. It is not allowed to become an
|
||||
unbounded request, hold an HTTP connection indefinitely, or bypass query
|
||||
budgets. A materialization that exceeds its approved budget stops with a
|
||||
specific, resumable status rather than the generic “temporarily unavailable”
|
||||
response.
|
||||
|
||||
## Read and write separation
|
||||
|
||||
The ingestion plane is append-oriented: validate, compress, checksum, sync,
|
||||
catalogue, acknowledge. It does not wait for query indexing.
|
||||
|
||||
The read plane is projection-oriented: select only authorized organization,
|
||||
signal, and time partitions; use exact rollups where semantically valid; merge
|
||||
partition-local partial results; then apply the final sort and limit. Query
|
||||
planning limits partition fan-out and reports the raw bytes, materializations,
|
||||
indexes, permissions, and expected cold work in `explain`.
|
||||
|
||||
This preserves SQLite's operational simplicity while allowing concurrent
|
||||
writers across independent partition files. It also creates a fair test of
|
||||
SQLite itself: if partition-local insertion remains the measured bottleneck
|
||||
after index review, the same raw segments and partition contract can be used
|
||||
to compare another embedded representation.
|
||||
|
||||
## What this does not solve automatically
|
||||
|
||||
- A memory tail improves visibility, not durable projector throughput.
|
||||
- Lazy materialization trades continuous background cost for first-query cost.
|
||||
- Too many tiny partitions increase file, migration, and planning overhead.
|
||||
- Cross-partition summaries need deterministic mergeable aggregate state.
|
||||
- Agent rules still consume bounded local CPU and memory, and central
|
||||
heartbeat checks still run when no user is online.
|
||||
- Cross-source rules require reviewed central work and cannot be reduced to
|
||||
one agent's local stream.
|
||||
- Agent compromise or suppression remains a trust boundary; authentication
|
||||
proves which enrolled source reported an event, not that the host itself was
|
||||
honest.
|
||||
- Historical regular-expression or high-cardinality queries still require
|
||||
strict scan and memory budgets.
|
||||
- Application-supplied sensitive data remains governed by descriptor,
|
||||
authorization, retention, and export policy regardless of storage tier.
|
||||
|
||||
## Staged proof
|
||||
|
||||
1. Add aggregate cost-centre instrumentation without changing behavior.
|
||||
2. Generalize the existing budgeted cold-segment reader to prove authorized
|
||||
bounded queries over selected hot raw segments; compare every result with
|
||||
the current SQLite projection.
|
||||
3. Add a strict partition catalogue and build one signal/day materialization
|
||||
beside the current projection. Differentially test queries, late data,
|
||||
duplicates, corruption, cancellation, and restart.
|
||||
4. Add the bounded recent-memory view and prove overlap deduplication.
|
||||
5. Move one single-source rule to agent evaluation and one observed dashboard
|
||||
to a shared lease. Differentially compare the rule with the current server
|
||||
evaluator and prove that many dashboard viewers do not duplicate work.
|
||||
6. Prove agent restart, offline spool, sequence gaps, duplicate transitions,
|
||||
changed rule versions, unavailable-node detection, and lazy evidence
|
||||
verification. Keep one reviewed cross-source rule central.
|
||||
7. Run same-host 100,000- and one-million-observation comparisons, then the
|
||||
four-CPU/eight-GiB release campaign.
|
||||
8. Stop universal row projection only after the adaptive path reproduces the
|
||||
current query, incident, retention, rebuild, and authorization behavior.
|
||||
|
||||
The first slice of step 2 is implemented as a logs-only, non-public candidate
|
||||
that reads both hot and cold retained batches under the ordinary typed-query
|
||||
budgets. Differential tests cover filters, sorting, summaries, regular
|
||||
expressions, resource scope, an absent projection, and incomplete retention
|
||||
transitions. The production projection remains the oracle. This candidate
|
||||
holds the organization lock during its scan and therefore is not yet the
|
||||
catalogue-snapshot or shared-lease implementation described above.
|
||||
|
||||
The published capacity gate is not weakened to make this design pass. If the
|
||||
product intentionally changes first-query behavior, that contract and its
|
||||
separate warm-query boundary must be reviewed explicitly before the fixture is
|
||||
changed.
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Agent boundary
|
||||
|
||||
The Observatory agent is an unprivileged outbound collector. Its local JSON
|
||||
configuration is authoritative: the server cannot add a path, collector,
|
||||
command, socket, or environment value remotely.
|
||||
|
||||
Configuration and the enrolled credential remain separate root-owned
|
||||
mode-`0600` regular files under `/etc`. The example systemd unit uses
|
||||
`LoadCredential=` to expose private, read-only runtime copies to the dedicated
|
||||
service account. `observatory agent --systemd-credentials` confines both paths
|
||||
to direct children of systemd's `CREDENTIALS_DIRECTORY`; the agent itself does
|
||||
not run as root. Its service account receives read access only to the explicitly
|
||||
selected Caddy, application, and Tend streams. It does not need a Docker socket,
|
||||
an inbound port, a shell plugin, or write access to the source logs.
|
||||
|
||||
The producer and its operator remain authoritative for what a source log is
|
||||
allowed to contain. Observatory is not a universal DLP system and cannot infer
|
||||
an organization's private vocabulary or prove that a renamed field is safe.
|
||||
The initial adapters therefore use a documented fixed field set before durable
|
||||
spooling, plus a small source-specific `sensitive_fields` list. This is a
|
||||
defence-in-depth boundary and a useful safe default—not permission to log a
|
||||
secret upstream. An absent list means the privacy-minimized fields below, and
|
||||
Observatory never widens it remotely.
|
||||
|
||||
- Caddy: method, query-free path, status, response bytes, duration, and a
|
||||
syntactically bounded request ID. The shipped Caddy example appends that ID
|
||||
from the completed response as a dedicated top-level field, then removes
|
||||
the complete request-header and response-header maps, client addresses,
|
||||
user identity, and the URI query before the edge record reaches disk;
|
||||
- Web Foundations requestlog: method, normalized route, status, response bytes,
|
||||
duration, authorization outcome, and request ID;
|
||||
- Tend: the strict version-1 bounded activation and rollback event fields;
|
||||
unknown, malformed, unsafe, oversized, or trailing data is rejected.
|
||||
|
||||
The optional `linux_metrics` source reads aggregate CPU ticks, load, uptime,
|
||||
memory, network counters, named filesystem capacity, selected cgroup v2
|
||||
counters, and processes named by explicit PID files. It never enumerates all
|
||||
processes, reads command lines or environments, invokes a shell, or opens a
|
||||
collector port. Configuration assigns stable public labels so local paths and
|
||||
PIDs are not stored as telemetry attributes. A disappearing selected process
|
||||
or cgroup emits an `up=0` metric while other host evidence remains durable.
|
||||
|
||||
Cookies, authorization headers, request and response bodies, arbitrary
|
||||
headers, and unknown JSON fields never enter the native batch. By default,
|
||||
raw query strings, client addresses, referrers, user agents, and anonymous
|
||||
session identifiers do not enter it either. An application whose documented
|
||||
privacy policy and operating need permit richer evidence may opt in per source:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "caddy_json",
|
||||
"path": "/var/log/caddy/example-access.jsonl",
|
||||
"stream_id": "caddy-access",
|
||||
"sensitive_fields": ["client_ip", "query", "referrer", "user_agent"]
|
||||
}
|
||||
```
|
||||
|
||||
`caddy_json` accepts `client_ip`, `query`, `referrer`, and `user_agent`.
|
||||
`requestlog_jsonl` accepts those fields plus `session_id`. Unsupported and
|
||||
duplicate names fail configuration validation. Values remain bounded, and IP
|
||||
addresses must parse as IPv4 or IPv6. Selected values enter Observatory as
|
||||
unknown sensitive, high-cardinality, unindexed raw fields until an authorized
|
||||
schema review explicitly classifies them. Query and referrer values can carry
|
||||
credentials or personal data even when collection is disclosed; filter known
|
||||
secret-bearing parameters at the producer, restrict access, and choose a
|
||||
retention window appropriate to the application's policy. The copyable
|
||||
`Caddyfile.sensitive-access-log` demonstrates the intended producer boundary.
|
||||
|
||||
Source scope is not read from any log; it comes from the server-side enrollment
|
||||
record after credential verification.
|
||||
|
||||
## Batching and delivery cadence
|
||||
|
||||
`flush_interval` is the maximum delay between collection cycles while the
|
||||
agent is healthy. `batch_records` caps the number of observations read into
|
||||
one durable batch for one stream during that cycle; it is not a reason to wait
|
||||
for a quiet stream to fill. Each batch is committed to the local spool before
|
||||
its source cursor advances, and delivery removes it only after an exact server
|
||||
acknowledgement.
|
||||
|
||||
Larger batches amortize filesystem, compression, HTTPS, and server admission
|
||||
costs. A shorter interval reduces live-view latency. The checked-in profile
|
||||
uses up to 5,000 records once per second. Operators can select any validated
|
||||
combination from 1 to 5,000 records and 100 milliseconds to one minute based
|
||||
on source volume and desired freshness. The tailer also caps bytes read during
|
||||
one cycle, so an existing multi-gigabyte file is caught up over bounded cycles
|
||||
rather than loaded into memory at once.
|
||||
|
||||
The file tailer reads at most 4 MiB of source data per cycle while the checked-
|
||||
in server accepts at most 32 MiB per authenticated request. That headroom
|
||||
accounts for native-batch JSON framing without making request memory
|
||||
unbounded. The server also admits at most eight concurrent authenticated
|
||||
ingestion bodies. A future byte-aware batcher should make the source/request
|
||||
relationship explicit for custom profiles that choose different ceilings.
|
||||
|
||||
The spool fails closed at its configured byte or age budget. A server
|
||||
acknowledgement must match the exact source, stream, sequence, and canonical
|
||||
logical-batch digest before the agent removes its local batch. The local spool
|
||||
envelope retains a separate digest because it also contains the cursor
|
||||
checkpoint; private storage encodings are not treated as protocol identities.
|
||||
Each pending envelope carries
|
||||
a bounded cursor checkpoint. Startup applies those checkpoints before it reads
|
||||
new bytes, so a crash between the spool commit and state-file replacement
|
||||
cannot duplicate a different payload under the same sequence. Collected records
|
||||
advance only after that envelope is durable; deliberately discarded complete
|
||||
records advance only after their bounded counters and cursor are durably saved.
|
||||
|
||||
Delivery uses the versioned native v2 endpoint. The agent supplies bounded
|
||||
stream, sequence, signal, exact encoded-body digest, canonical logical-batch
|
||||
digest, record/byte counts, and first/last observation times as request
|
||||
metadata. Source and organization scope still come only from the enrolled
|
||||
credential. On an exact retry the server reads and hashes the complete bounded
|
||||
body but does not decode, recompress, recatalogue, or reproject it. Time ranges
|
||||
help select storage and query partitions; overlapping ranges and repeated
|
||||
values remain valid and are never treated as record-level duplicates.
|
||||
|
||||
## Local alert rules
|
||||
|
||||
An agent may evaluate an optional, locally configured rule against one exact
|
||||
log batch after that batch is durable. The first supported rule shape is
|
||||
deliberately narrow: one `caddy_json` or `requestlog_jsonl` stream, one or more
|
||||
typed `where` filters, no sort, summary, or historical window, and a bounded
|
||||
minimum match count. It uses the same typed filter implementation as the
|
||||
central query evaluator.
|
||||
|
||||
```json
|
||||
{
|
||||
"alert_rules": [
|
||||
{
|
||||
"version": 1,
|
||||
"id": "http-failures",
|
||||
"revision": 1,
|
||||
"stream_id": "application-request",
|
||||
"query": "logs | where status >= 500 | limit 10",
|
||||
"minimum_matches": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The identifier and revision must name an enabled, source-scoped server rule
|
||||
whose saved query has the same intended semantics. Configuration remains
|
||||
local; the server cannot install or edit this rule. After the raw batch is
|
||||
acknowledged, the agent reports only `matched`, `clear`, or `error`, its local
|
||||
rule identity, the exact raw segment identity, the batch time range, and a
|
||||
monotonic sequence. It sends no telemetry values in that transition.
|
||||
|
||||
The raw spool entry is removed only after both acknowledgements match. If the
|
||||
transition request fails, retrying first replays the same raw batch as an
|
||||
idempotent no-op and then replays the same transition. During this
|
||||
compatibility phase the server stores the authenticated source result but does
|
||||
not mutate incident state from it. The existing central evaluator remains the
|
||||
oracle until differential tests and a production soak demonstrate equivalent
|
||||
results and failures. Multi-batch windows, cross-source rules, and server-owned
|
||||
incident confirmation remain central work in this preview.
|
||||
|
||||
The tailer preserves incomplete lines, discards oversized or malformed
|
||||
complete records without retaining their contents, caps read work per cycle,
|
||||
detects copy-truncation, and follows an unread inode through a rename in the
|
||||
configured file's directory. If the old inode has already disappeared, the
|
||||
agent records a discontinuity and begins the new file; it does not claim that
|
||||
unrecoverable bytes were observed.
|
||||
|
||||
An organization-authorized local administrator writes a short-lived
|
||||
enrollment token directly to a new mode-`0600` file. `observatory agent enroll`
|
||||
exchanges it once over HTTPS and creates the configured credential file without
|
||||
overwriting any existing secret. If the credential cannot be written, the
|
||||
client asks the server to revoke the newly enrolled source.
|
||||
|
||||
Create the source's resource hierarchy through the supported offline
|
||||
administration commands before issuing an enrollment. Each command acquires
|
||||
the same shared process lock as the live server, validates its exact parent
|
||||
scope, requires the actor's `organization.manage` grant, and returns only
|
||||
generated public identifiers. Normal hierarchy and enrollment administration
|
||||
can run while the server is live. Exclusive migrations still require the
|
||||
server to stop. Never edit the control database directly.
|
||||
|
||||
```sh
|
||||
sudo observatory admin project create \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--slug gamertan \
|
||||
--name 'Gamertan'
|
||||
|
||||
sudo observatory admin environment create \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--project-id PROJECT_ID \
|
||||
--slug production \
|
||||
--name 'Production'
|
||||
|
||||
sudo observatory admin service create \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--project-id PROJECT_ID \
|
||||
--environment-id ENVIRONMENT_ID \
|
||||
--slug web \
|
||||
--name 'Web application'
|
||||
```
|
||||
|
||||
Creating the hierarchy is intentionally incremental: a successfully created
|
||||
project remains a valid resource if a later environment or service command is
|
||||
rejected. Slugs are unique within their parent, and failed authorization or
|
||||
parent validation creates nothing.
|
||||
|
||||
```sh
|
||||
sudo observatory admin enrollment create \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--actor-user-id USER_ID \
|
||||
--source-id HOST_SOURCE_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--project-id PROJECT_ID \
|
||||
--environment-id ENVIRONMENT_ID \
|
||||
--service-id SERVICE_ID \
|
||||
--lifetime 15m \
|
||||
--output-file /etc/gamertan-observatory/new-agent-enrollment.json
|
||||
|
||||
sudo observatory agent enroll \
|
||||
--config /etc/gamertan-observatory/agent.json \
|
||||
--enrollment-file /etc/gamertan-observatory/new-agent-enrollment.json
|
||||
|
||||
sudo systemctl enable --now observatory-agent.service
|
||||
```
|
||||
|
||||
The token and credential never appear in command arguments: only their file
|
||||
paths do. Remove the enrollment-token file after a successful exchange. The
|
||||
copyable unit in `examples/observatory-agent.service` documents the supported
|
||||
unprivileged runtime boundary.
|
||||
|
||||
## Production dogfood profile
|
||||
|
||||
The checked-in `release/agent.json` and `release/observatory-agent.service`
|
||||
describe the first production profile. One host-scoped source credential is
|
||||
bound to the public application node. Locally authoritative collectors keep
|
||||
EQL's edge and application evidence in distinct streams and collect aggregate
|
||||
Linux and selected service-cgroup metrics. This scope does not imply that all
|
||||
telemetry belongs to the Observatory application merely because Observatory
|
||||
receives it.
|
||||
|
||||
The profile intentionally omits `sensitive_fields`. Client addresses, query
|
||||
strings, referrers, user agents, and session identifiers therefore remain out
|
||||
of the agent spool and server. They may be enabled later, per stream, only
|
||||
after the producer filter, privacy policy, access grants, and retention window
|
||||
have been reviewed. The capability remains supported; privacy minimization is
|
||||
the production default rather than an architectural prohibition.
|
||||
|
||||
The Observatory origin's own Caddy access log is not collected by this first
|
||||
profile. Ingesting the agent's requests from that log would create a perpetual
|
||||
self-observation loop. The profile does collect the bounded Tend event files
|
||||
for Gamertan, Sandwich Hime, and Observatory as three independent streams.
|
||||
Their application release state remains authoritative; ingestion is evidence,
|
||||
not a deployment dependency.
|
||||
|
||||
The service account has supplementary read-only membership in the `caddy` and
|
||||
`eqlwiki` groups, write access only to its private state directory, no inbound
|
||||
listener, no Docker socket, and no shell. `PartOf=gamertan-observatory.service`
|
||||
restarts the agent when a Tend activation moves the shared `current` binary,
|
||||
so server and agent do not drift across releases.
|
||||
@@ -0,0 +1,201 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Architecture
|
||||
|
||||
Observatory preserves three independent evidence layers:
|
||||
|
||||
1. an agent's local spool retains unacknowledged batches;
|
||||
2. immutable raw segments are committed and checksummed before acknowledgement,
|
||||
then move unchanged from hot to cold forensic storage;
|
||||
3. per-organization SQLite projections are disposable query accelerators.
|
||||
|
||||
Durable acceptance and query visibility are deliberately separate. A source
|
||||
may receive an exact acknowledgement as soon as its immutable segment and
|
||||
control-plane identity are durable. A bounded background projector then makes
|
||||
that evidence visible to queries. The authenticated interface reports this
|
||||
lag for the selected organization instead of pretending that accepted and
|
||||
indexed mean the same thing.
|
||||
|
||||
The control database stores Web Foundations identities, organizations, scoped
|
||||
access grants, source credential digests, sequence watermarks, segment
|
||||
projection state, dashboards, alert rules, incident metadata, and optional
|
||||
user-owned Web Push endpoints and their organization-scoped mappings. Every
|
||||
organization's query projection is stored in a distinct SQLite database.
|
||||
Authorization is applied before opening that projection.
|
||||
|
||||
Saved queries and dashboards live in the control database, but every key and
|
||||
foreign-key relationship includes the organization identifier. Saved query
|
||||
text is parsed into the same typed AST at write time; both representations are
|
||||
stored and revalidated for exact semantic agreement when read. Updates use
|
||||
optimistic revisions. Dashboard panels reference only saved queries from the
|
||||
same organization. Source-control exports deliberately omit tenant IDs,
|
||||
operator IDs, timestamps, and internal revisions.
|
||||
|
||||
The web interface is an ordinary `net/http` application rendered through
|
||||
compiler-generated Sandwich Hime components. The application buffers complete
|
||||
HTML before committing status and headers. Hime-san remains a development and
|
||||
CI tool; the service links only the pinned Sando runtime. Content-hashed CSS
|
||||
and JavaScript are embedded in the binary and served under immutable URLs.
|
||||
JavaScript opens an authenticated SSE connection only to learn that newer data
|
||||
exists. The stream carries no telemetry or resource labels, and dropping it
|
||||
does not remove any navigation, query result, table, or authentication
|
||||
function.
|
||||
|
||||
The text language and visual builder both produce the same versioned typed AST.
|
||||
Planning receives an already-authorized organization/resource scope separately
|
||||
from that AST; query text cannot select a tenant. Explain output names the
|
||||
scoped projected source, reviewed descriptors and indexes, estimated scan,
|
||||
cache eligibility, budgets, and required permissions. Unknown fields remain
|
||||
raw-query candidates but default to sensitive, high-cardinality, and unindexed.
|
||||
Execution opens only the already-authorized organization's projection in
|
||||
query-only mode. Server-owned project, environment, and service constraints
|
||||
are added independently of the AST; the executor applies typed comparisons,
|
||||
bounded RE2 regular expressions, time windows, grouping buckets, aggregates,
|
||||
sorting, row limits, and separate time, decoded-byte, and memory ceilings.
|
||||
Only default safe columns and fields explicitly referenced by the query enter
|
||||
the result table. `window 24h` is a lookback; `window(5m)` is a distinct
|
||||
aggregation bucket in the versioned AST.
|
||||
|
||||
Alert rules reference a saved query in the same organization. A single
|
||||
evaluator claims each due rule by advancing its next-evaluation time before it
|
||||
opens the organization's query projection, preventing concurrent duplicate
|
||||
evaluation. The ordinary query budget remains authoritative. A configured row
|
||||
threshold determines whether an evaluation matches; consecutive matches move
|
||||
an incident from pending to firing. Query failure records only the bounded
|
||||
code `query_unavailable` and leaves an existing incident unchanged. Incident
|
||||
state and its append-only event sequence live in the control database, contain
|
||||
no query result or telemetry value, and remain organization-keyed on every
|
||||
relationship.
|
||||
|
||||
Web Push remains downstream of durable incident state. Only a committed
|
||||
transition into `firing` enters a bounded, deduplicated in-memory queue. The
|
||||
dispatcher reauthorizes `incidents.read`, encrypts one fixed generic sentence,
|
||||
and uses a redirect-free HTTPS client that rejects non-public DNS results.
|
||||
Vendor delivery cannot block rule evaluation, and delivery failure cannot
|
||||
change incident state. The service worker ignores payload content and opens
|
||||
the authenticated application rather than carrying a tenant or incident
|
||||
identifier through the push relay.
|
||||
|
||||
Unknown attribute keys produce descriptor proposals only after their raw
|
||||
segment is committed. Proposal evidence is aggregated once per segment using
|
||||
the segment digest, organization, and field as the idempotency identity. The
|
||||
control database stores counts, estimated bytes, inferred type, first/last
|
||||
observation times, and a generic example query—not observed values. Built-in
|
||||
reviewed fields are excluded. Proposals remain sensitive, high-cardinality,
|
||||
and unindexed; proposal persistence does not activate a descriptor.
|
||||
|
||||
Descriptor activation is an explicit organization-authorized operation. A
|
||||
reviewed descriptor is loaded from a bounded private JSON file; unknown
|
||||
properties and weak or symlinked files are rejected. Observatory copies the
|
||||
currently active descriptors into a new per-organization projection version,
|
||||
builds a typed custom-field index by scanning the disposable observation
|
||||
projection, and changes the active version in the same SQLite transaction.
|
||||
Invalid historical values are omitted rather than coerced. Existing and newly
|
||||
ingested observations use the active registry after commit, while prior index
|
||||
tables remain available for recovery. The control-database proposal status is
|
||||
held under a write claim during the build and acknowledged only after the
|
||||
projection commit; retry repairs an interrupted acknowledgement idempotently.
|
||||
|
||||
## Ingestion transaction
|
||||
|
||||
1. Authenticate the source credential and load its server-owned scope.
|
||||
2. For native v2, validate the bounded transport envelope. If it exactly
|
||||
matches the current acknowledged stream watermark, hash and discard the
|
||||
complete body, recheck the watermark under the source lock, and return the
|
||||
retained acknowledgement without decoding or writing it again.
|
||||
3. For a new batch, validate decompressed size, record count, field bounds,
|
||||
timestamps, signal type, sequence, and every envelope/body field.
|
||||
4. Commit a content-addressed zstd raw segment with `fsync` and atomic rename.
|
||||
5. Serialize organization quota admission and reject the exact segment before
|
||||
cataloguing it when the approved quota is exhausted.
|
||||
6. In one control-database transaction, catalogue the committed segment and
|
||||
advance the `(source, stream)` watermark with its bounded envelope metadata.
|
||||
7. Acknowledge the exact sequence,
|
||||
raw-segment digest, and canonical logical-batch digest.
|
||||
8. Outside the acknowledgement path, group a bounded number of one
|
||||
organization's pending segments into one projection transaction. Project
|
||||
at most four distinct organizations concurrently, while keeping each
|
||||
tenant's base projection, active custom index, metric rollups, and replay
|
||||
ledgers in its own transaction.
|
||||
9. Aggregate unknown-field proposal evidence without retaining observed
|
||||
values, then mark each projected segment complete in the control database.
|
||||
|
||||
The dogfood server bounds an authenticated request at 32 MiB and admits at
|
||||
most eight ingestion bodies concurrently. File agents read no more than 4 MiB
|
||||
of source text into a cycle. These independent bounds preserve batching
|
||||
headroom while preventing source credentials from creating unbounded request
|
||||
memory pressure.
|
||||
|
||||
A retry of the same sequence and digest is idempotently acknowledged even
|
||||
while projection is pending. Reusing a sequence with different bytes or
|
||||
submitting an older sequence is rejected. If projection fails, the durable
|
||||
segment remains pending and the projector retries it without holding the
|
||||
source or listener hostage. Work is bounded and selected fairly across
|
||||
organizations so one tenant's corrupt segment cannot prevent other tenants
|
||||
from advancing.
|
||||
|
||||
Server startup first reconciles raw objects and control metadata without
|
||||
decoding already catalogued objects, then opens the listener and drains
|
||||
pending projections in the background. The one-shot `check` and `migrate`
|
||||
commands retain a blocking recovery path for offline verification.
|
||||
|
||||
Every projected metric sample also updates a five-minute aggregate in the same
|
||||
organization projection transaction. Rollups retain exact count, sum, minimum,
|
||||
and maximum plus a deterministic bounded histogram for explicitly approximate
|
||||
percentiles. Only reviewed, non-sensitive, bounded-cardinality dimensions with
|
||||
the `metric` retention class enter that longer-lived projection. Unknown and
|
||||
sensitive attributes remain raw-only. Summary queries that can be represented
|
||||
faithfully select the rollup explicitly in their explain plan; unsupported
|
||||
query shapes continue to use raw samples.
|
||||
|
||||
The server applies retention after recovery and once per hour. Projected rows
|
||||
expire at their signal's hot cutoff. Once every record in a segment is outside
|
||||
that hot window, the control database first records an exact cold destination,
|
||||
then the unchanged zstd object is atomically renamed into the private cold
|
||||
tree, and finally its catalog path and tier advance. Recovery completes either
|
||||
side of that rename idempotently. Cold segments remain checksum-verifiable and
|
||||
queryable through a slower scan path with the ordinary authorization and
|
||||
resource budgets. Cold raw evidence is preserved indefinitely by default.
|
||||
Only a policy with `delete_cold_raw` explicitly enabled marks segments beyond
|
||||
its final cold cutoff as retiring and removes them through a separate
|
||||
crash-recoverable lifecycle. Resolved incidents and security audit evidence
|
||||
use the configured evidence window. Organization overrides may shorten
|
||||
defaults. Extending a finite server policy to indefinite preservation requires
|
||||
an exact organization approval and an enforceable storage quota.
|
||||
|
||||
The agent has a separate durability boundary. It commits a private,
|
||||
checksummed zstd envelope before attempting HTTPS delivery and removes it only
|
||||
after an acknowledgement binds the exact logical batch. Its envelope digest
|
||||
continues to protect the checkpoint and compressed local bytes. Collection
|
||||
adapters use fixed whitelists so known credentials and high-risk fields are
|
||||
absent before the spool write. Server configuration cannot alter the agent's
|
||||
selected local sources.
|
||||
|
||||
OTLP/HTTP accepts the standard protobuf wire shape for logs, metrics, and
|
||||
traces without enabling gRPC, a collector listener, or remote configuration.
|
||||
The server authenticates the enrolled source before decoding, applies limits
|
||||
to both compressed and decompressed bytes, removes known credential-bearing
|
||||
attribute keys, and injects the source's stored resource scope. OTLP payloads
|
||||
cannot select an organization, project, environment, or service. Automatic
|
||||
stream sequences are assigned under a per-source lock; native batches retain
|
||||
their explicit replay-safe sequence and digest acknowledgement contract.
|
||||
|
||||
Linux host metrics are collected in the same unprivileged agent cycle and
|
||||
enter the same durable spool. The collector reads a fixed set of bounded
|
||||
`/proc` and cgroup v2 files, named filesystem statistics, and only process IDs
|
||||
resolved through explicitly configured PID files. Files are required to be
|
||||
regular and non-symlinked; cgroup traversal is confined component-by-component
|
||||
beneath the configured root. Local paths, raw PIDs, command lines, and
|
||||
environment values are not projected.
|
||||
|
||||
## Tenant boundary
|
||||
|
||||
Payloads contain no organization, project, environment, or service selector.
|
||||
Those values come from the enrolled source record after credential
|
||||
verification. Published Web Foundations Preview 3 supplies user identities,
|
||||
personal and shared organizations, resources, teams, invitations, and scoped
|
||||
query grants, plus the forced initial-password rotation contract. Platform
|
||||
operator status is a separate policy and does not authorize telemetry access.
|
||||
A query request names a desired resource, but the server independently
|
||||
authorizes that scope and computes sensitive-field access before it constructs
|
||||
the planner input.
|
||||
@@ -0,0 +1,152 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Local platform bootstrap
|
||||
|
||||
Bootstrap is deliberately local and single-use. It creates the first user,
|
||||
marks that user as a platform operator, creates their personal organization,
|
||||
and grants owner access only inside that organization. Platform operation does
|
||||
not imply access to another organization's telemetry.
|
||||
|
||||
Production configuration and the generated password file must be root-owned,
|
||||
mode `0600`, regular files. Observatory creates the file exclusively and
|
||||
refuses an existing path. The password is never a command argument, output
|
||||
field, manifest value, log value, or persisted cleartext value.
|
||||
|
||||
```sh
|
||||
sudo observatory admin bootstrap \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--username operator \
|
||||
--email operator@example.com \
|
||||
--display-name 'First Operator' \
|
||||
--generate-password-file /root/observatory-bootstrap-password
|
||||
```
|
||||
|
||||
The command returns only the opaque user and personal-organization IDs plus
|
||||
`password_change_required: true`. Read the file locally, sign in, and replace
|
||||
the temporary credential. That session is restricted to password rotation,
|
||||
logout, health, and immutable application-shell assets. A successful change
|
||||
revokes every session, clears the browser cookie, and requires a fresh login.
|
||||
Delete the generated file only after the replacement login succeeds.
|
||||
|
||||
## Recover local access
|
||||
|
||||
If an operator loses a credential, recover it only from the host. Observatory
|
||||
does not expose a password-reset route, email flow, recovery token API, or
|
||||
registration window:
|
||||
|
||||
```sh
|
||||
sudo env CREDENTIALS_DIRECTORY=/run/credentials/gamertan-observatory.service \
|
||||
observatory admin user reset-password \
|
||||
--config /run/credentials/gamertan-observatory.service/server.json \
|
||||
--systemd-credential-config \
|
||||
--identifier speelman \
|
||||
--generate-password-file /root/observatory-password-recovery
|
||||
```
|
||||
|
||||
The command creates the output file exclusively as a root-owned regular file
|
||||
with mode `0600`. It then atomically installs that one-time credential, restores
|
||||
the password-change requirement, revokes every session, and appends a generic
|
||||
audit event without the credential or its path. If the database operation
|
||||
fails, the generated file is removed. Read the file locally, complete the
|
||||
server-rendered password-change form, and delete the file after the new login
|
||||
succeeds. Never place the credential in shell arguments, chat, logs, manifests,
|
||||
or deployment state.
|
||||
|
||||
There is no public first-user registration window. The bootstrap command is
|
||||
single-use, local, and refuses a second platform user. If an operator already
|
||||
has an appropriately generated password, `--password-file` remains available;
|
||||
that explicitly supplied credential is treated as permanent and does not set
|
||||
the forced-change flag. The two password flags are mutually exclusive.
|
||||
|
||||
If bootstrap fails after creating a generated credential, Observatory removes
|
||||
the file after revalidating its exact path, type, owner, and mode. A process
|
||||
crash between file creation and database bootstrap can leave a credential with
|
||||
no account; inspect the control database and remove that exact file before a
|
||||
reviewed retry. Never replace the path with a symlink or broaden its mode.
|
||||
|
||||
When the installed service loads its configuration and Web Push key through
|
||||
systemd credentials, use that same confined runtime view rather than copying
|
||||
or changing credential modes:
|
||||
|
||||
```sh
|
||||
sudo env CREDENTIALS_DIRECTORY=/run/credentials/gamertan-observatory.service \
|
||||
observatory admin bootstrap \
|
||||
--config /run/credentials/gamertan-observatory.service/server.json \
|
||||
--systemd-credential-config \
|
||||
--username operator \
|
||||
--email operator@example.com \
|
||||
--display-name 'First Operator' \
|
||||
--generate-password-file /root/observatory-bootstrap-password
|
||||
```
|
||||
|
||||
The systemd mode accepts only a direct child of the declared credential
|
||||
directory, owned by root or the current service user, with mode `0400`, `0440`,
|
||||
or `0600`. It is mutually exclusive with the local non-root development flag.
|
||||
Every command that loads the server configuration—including user, invitation,
|
||||
resource, enrollment, descriptor, retention, query, import, export, and
|
||||
migration operations—accepts the same `--systemd-credential-config` boundary.
|
||||
Operators do not need to copy a runtime credential or weaken its mode merely
|
||||
to run an administrative command.
|
||||
|
||||
The browser login form carries an independent, short-lived, unpredictable
|
||||
token in a Secure, HttpOnly, SameSite=Strict cookie and the rendered form.
|
||||
Authenticated forms use separate purpose-bound tokens derived from the opaque
|
||||
session. These tokens are the primary CSRF proof for ordinary server POSTs.
|
||||
`Origin` and Fetch Metadata are independent contradiction checks: explicitly
|
||||
cross-site requests and tokenless requests remain rejected, while missing or
|
||||
opaque metadata from privacy tools cannot prevent a valid form submission.
|
||||
Failures return a fresh server-rendered form with bounded guidance and never
|
||||
echo the submitted identifier or password.
|
||||
|
||||
## Add a user through an invitation
|
||||
|
||||
Later users are provisioned locally. Each receives an automatically created
|
||||
personal organization, but no access to anyone else's telemetry. Create a
|
||||
private password file without placing the password in shell history, then run:
|
||||
|
||||
```sh
|
||||
sudo observatory admin user create \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--username responder \
|
||||
--email responder@example.com \
|
||||
--display-name 'Incident Responder' \
|
||||
--password-file /etc/gamertan-observatory/responder-password
|
||||
```
|
||||
|
||||
An owner of the destination organization creates an expiring, single-use
|
||||
invitation. The owner must supply their opaque user and organization IDs:
|
||||
|
||||
```sh
|
||||
sudo observatory admin invitation create \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--email responder@example.com \
|
||||
--lifetime 15m \
|
||||
--output-file /etc/gamertan-observatory/responder-invitation
|
||||
```
|
||||
|
||||
The token is written only to the new mode-`0600` file. Existing paths,
|
||||
symlinks, multiline values, and weak files fail closed; a database invitation
|
||||
is cancelled automatically if its token file cannot be persisted. Command
|
||||
output contains metadata, never the token. Transfer the file through a
|
||||
separately protected channel and accept it locally:
|
||||
|
||||
```sh
|
||||
sudo observatory admin invitation accept \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--user-id INVITED_USER_ID \
|
||||
--invitation-file /etc/gamertan-observatory/responder-invitation
|
||||
```
|
||||
|
||||
Acceptance requires the provisioned user's exact normalized email address.
|
||||
The token cannot be reused. Remove both password and invitation files after
|
||||
the user confirms access. The preview intentionally has no public registration,
|
||||
email-delivery protocol, or self-service invitation UI.
|
||||
|
||||
Bootstrap and later user provisioning each create a user, personal
|
||||
organization, and access grant through separately checked storage operations.
|
||||
Until that sequence becomes one control-database transaction, retain a
|
||||
verified control-database backup before provisioning identities. If a command
|
||||
reports a partial provisioning failure, stop and restore or inspect the
|
||||
database rather than blindly retrying it.
|
||||
@@ -0,0 +1,186 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Capacity campaign
|
||||
|
||||
Observatory's release capacity gate is executable and intentionally narrow. It
|
||||
measures this implementation on one 4-vCPU/8-GiB Linux boundary; it is not a
|
||||
claim of generic superiority or unlimited scale.
|
||||
|
||||
The campaign:
|
||||
|
||||
- sustains 2,000 mixed observations per second for one hour across four
|
||||
equally weighted organizations so concurrent isolation and all four
|
||||
constrained CPUs are exercised;
|
||||
- absorbs 10,000 mixed observations per second for 60 seconds;
|
||||
- requires p95 ingestion-to-query visibility below two seconds;
|
||||
- expands the primary organization to at least ten million observations and
|
||||
runs indexed log and metric-rollup views over its last 24 hours, requiring
|
||||
every p95 below three seconds;
|
||||
- places 72 ordered batches at the edge of the 72-hour outage budget, replays
|
||||
them, recognizes a duplicate, and removes local evidence only after an exact
|
||||
logical-batch acknowledgement;
|
||||
- archives expired raw objects unchanged, queries cold evidence, and removes
|
||||
only material beyond the final cold cutoff; and
|
||||
- records bounded RSS and disk growth without emitting a path, host, user,
|
||||
credential, source value, or telemetry value.
|
||||
|
||||
The aggregate release gate is not a claim that one organization can ingest
|
||||
10,000 observations per second through its deliberately serialized SQLite
|
||||
projection. The same fixture accepts `-primary-phase-weight 7` for a separate
|
||||
70-percent hot-tenant diagnostic and records that weight in its JSON report.
|
||||
The ten-million-observation query corpus is still built in the primary
|
||||
organization after the timed phases.
|
||||
|
||||
The workload producer and visibility observer run independently. The observer
|
||||
queries each primary log batch while traffic continues and records when that
|
||||
timestamp reaches the read model; it never makes the producer wait for its own
|
||||
query. The achieved ingest rate therefore measures scheduled durable
|
||||
acceptance, while the separate visibility distribution still captures
|
||||
background projection lag under the same load.
|
||||
|
||||
Run a short development proof with:
|
||||
|
||||
```sh
|
||||
./scripts/capacity-campaign.sh
|
||||
```
|
||||
|
||||
The ordinary public-preview gate runs that development proof after the
|
||||
complete deterministic verifier and five-second smoke passes for each fuzz
|
||||
target:
|
||||
|
||||
```sh
|
||||
./scripts/preview-gate.sh
|
||||
```
|
||||
|
||||
This is the default dogfood gate. The hour, ten-million-observation, extended
|
||||
fuzz, fleet, and 24-hour campaigns are milestone tools. They are not required
|
||||
for every interface iteration and must not be implied by a preview that did
|
||||
not run them.
|
||||
|
||||
The release campaign must run inside an externally enforced four-CPU,
|
||||
8-GiB cgroup and requires an explicit mode:
|
||||
|
||||
```sh
|
||||
OBSERVATORY_CAPACITY_MODE=release ./scripts/capacity-campaign.sh
|
||||
```
|
||||
|
||||
The resulting JSON is aggregate evidence. A passing development run does not
|
||||
replace a run against the exact packaged release commit, its immutable digest,
|
||||
or the later medium-fleet soak. A campaign that completes every stage but
|
||||
misses a terminal rate, visibility, query, or memory gate still emits the same
|
||||
aggregate report with `pass: false` before returning a failure. Earlier stage
|
||||
failures remain fail-closed and may produce only the bounded stage marker and
|
||||
error. Individual visibility samples may be observed for up to 30 seconds so
|
||||
one outlier does not erase the phase evidence; the release boundary remains
|
||||
the aggregate p95 below two seconds.
|
||||
|
||||
Report schema version 2 adds the backlog present immediately after synthetic
|
||||
fill, projection-drain duration, storage bytes by raw/control/projection class,
|
||||
and primary-projection SQLite page bytes by checked-in schema object. These are
|
||||
aggregate performance counters, not telemetry export.
|
||||
|
||||
## August 17 constrained-run observations
|
||||
|
||||
The first full-duration campaign found a concurrent first-write directory
|
||||
creation race after completing its one-hour sustain phase. The directory
|
||||
validator was corrected to tolerate `EEXIST` only long enough to re-inspect the
|
||||
exact private, non-symlink directory chain, and a 64-writer regression now
|
||||
protects that boundary.
|
||||
|
||||
The corrected implementation was then exercised in a disposable Debian 12
|
||||
container with four CPUs, 8 GiB of memory, a 1,024-process ceiling, a read-only
|
||||
root filesystem, no network, all Linux capabilities dropped,
|
||||
`no-new-privileges`, and a non-root user. The container used the pinned image
|
||||
digest:
|
||||
|
||||
```text
|
||||
debian@sha256:936abff852736f951dab72d91a1b6337cf04217b2a77a5eaadc7c0f2f1ec1758
|
||||
```
|
||||
|
||||
That run completed 7,200,000 mixed observations in 3,600.38 seconds: about
|
||||
1,999.79 observations per second, within the one-percent sustain tolerance.
|
||||
It did not pass the burst gate. The 600,000-observation burst required
|
||||
227.73 seconds, about 2,634.66 observations per second rather than the required
|
||||
10,000. The container never reached its memory boundary or an OOM condition;
|
||||
its observed memory remained below 2 GiB. Aggregate block-I/O counters reached
|
||||
approximately 57.1 GB while retained test data was approximately 8.1 GB,
|
||||
identifying synchronous durable-write amplification as the next measured
|
||||
engineering boundary rather than CPU or memory exhaustion.
|
||||
|
||||
Once the burst miss was conclusive, the disposable run was stopped during its
|
||||
ten-million-observation fill. Its dataset was removed by the fixture's normal
|
||||
signal cleanup; timestamped stage logs and container exit evidence were
|
||||
retained. Queries, outage replay, retention, and the final aggregate report
|
||||
from that run are therefore not release evidence. Observatory will not lower
|
||||
or relabel the published gate: durable-write grouping and projection work must
|
||||
be measured, reviewed, and followed by a complete exact-candidate rerun.
|
||||
|
||||
## August 18 query-boundary observation
|
||||
|
||||
A later constrained candidate reached the complete ten-million-observation
|
||||
primary corpus but stopped at the first required query. The indexed
|
||||
`status >= 500` path still scanned and grouped the matching raw log rows, and
|
||||
the ten-second execution budget expired before it produced the route/window
|
||||
summary. No aggregate report was emitted, so that run is failure evidence—not
|
||||
a capacity pass.
|
||||
|
||||
The response is an additive, exact five-minute status/route projection. It is
|
||||
updated in the same SQLite transaction as the primary projection, uses a
|
||||
per-segment ledger for replay idempotence, backfills older projections once,
|
||||
and expires with the hot-log projection. A non-aligned lower window boundary
|
||||
still scans its exact raw fragment, bounded to less than five minutes. Queries
|
||||
that need cold evidence or do not match the narrow typed shape stay on the raw
|
||||
path.
|
||||
|
||||
Before another full release campaign, the implementation completed the entire
|
||||
development fixture and a separate one-million-observation diagnostic inside
|
||||
the release cgroup boundary. The diagnostic used four CPUs and 8 GiB of memory;
|
||||
the error summary completed five times with a 12.94 ms maximum sample, the
|
||||
ordinary indexed item view with a 4.59 ms maximum, and the metric rollup with a
|
||||
3.73 ms maximum. Maximum RSS was 205,545,472 bytes and the dataset was
|
||||
1,005,353,808 bytes. Those measurements are a scale diagnostic only. They do
|
||||
not replace the required one-hour, ten-million-observation exact-candidate
|
||||
campaign or its three-second p95 gate.
|
||||
|
||||
## August 18 asynchronous-projection diagnostic
|
||||
|
||||
After durable acknowledgement was separated from grouped background
|
||||
projection, a short local Linux/WSL2 development run exercised the same mixed
|
||||
fixture with a two-second sustain, two-second burst, 100,000-observation query
|
||||
corpus, four organizations, and a 500-millisecond visibility target. It was not
|
||||
run inside the release cgroup and is not release evidence.
|
||||
|
||||
The sustain reached about 2,530 observations per second with 31.92 ms p95
|
||||
ingest time and 201 ms p95 visibility. The burst reached about 10,787
|
||||
observations per second with 29.83 ms p95 ingest time and 404 ms p95
|
||||
visibility. The three required query shapes completed with p95 samples of
|
||||
2.264 ms, 1.992 ms, and 1.512 ms. Maximum RSS was approximately 208 MB and the
|
||||
dataset approximately 164 MB.
|
||||
|
||||
This diagnostic is evidence that acknowledgement-path projection writes were
|
||||
the measured burst bottleneck and that the revised shape is worth the full
|
||||
campaign. It does not close the four-CPU/eight-GiB gate, the one-hour sustain,
|
||||
the 60-second burst, the ten-million-observation corpus, outage replay, or the
|
||||
exact packaged-candidate requirement.
|
||||
|
||||
## August 18 exact-candidate boundary
|
||||
|
||||
The exact asynchronous-projection candidate was then run in the release
|
||||
four-CPU/eight-GiB container. It completed the one-hour sustain, burst, and
|
||||
ten-million-observation fill without an out-of-memory condition. After a
|
||||
ten-minute projection drain, 1,118 committed raw segments (922,788,512 bytes)
|
||||
remained pending and the oldest projection lag was 12 minutes 22 seconds. The
|
||||
campaign therefore failed closed before the query, outage-replay, retention,
|
||||
and terminal aggregate stages.
|
||||
|
||||
This is useful negative evidence: durable acceptance and bounded memory held,
|
||||
but projection throughput at that synthetic scale is not yet a supported
|
||||
claim. The result does not block lower-volume public dogfooding. It does block
|
||||
calling the release-scale capacity gate complete, and it is the reason that
|
||||
extended capacity remains a milestone campaign rather than the default
|
||||
preview loop.
|
||||
|
||||
The retained baseline comparisons, rejected multi-row and durability
|
||||
experiments, and ordered lower-level case studies are recorded in
|
||||
[PERFORMANCE.md](PERFORMANCE.md). Rejected experiments must not be repeated
|
||||
without a materially changed premise and a new same-host baseline.
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Saved queries and dashboards
|
||||
|
||||
Observatory persists query definitions before building the web interface on
|
||||
top of them. A saved query contains bounded human metadata, one organization,
|
||||
an optional project/environment/service scope, the reviewed query text, and
|
||||
the exact versioned typed AST produced from that text. Reads reparse the text,
|
||||
hydrate its duration fields, validate the stored AST, and require the two
|
||||
representations to match.
|
||||
|
||||
Dashboards are versioned ordered collections of at most 16 panels. Each panel
|
||||
selects one saved query and one deliberately small visualization contract:
|
||||
`table`, `stat`, or `timeseries`. Composite foreign keys prevent a panel from
|
||||
referencing another organization's query. Updates to saved queries and
|
||||
dashboards require their current revision so concurrent edits fail instead of
|
||||
silently overwriting one another.
|
||||
|
||||
Identifiers are generated from cryptographic randomness; randomness failure
|
||||
fails the write and never falls back to time or a predictable counter.
|
||||
|
||||
## Source-control export
|
||||
|
||||
`ExportDashboard` produces strict version-1 JSON containing the dashboard
|
||||
definition and each referenced query once. It keeps the stable definition IDs
|
||||
needed by panels, but omits organization IDs, operator user IDs, timestamps,
|
||||
internal revisions, and redundant stored AST bytes.
|
||||
|
||||
`observatory export` performs the same organization-authorized read from the
|
||||
local server data directory. `observatory import` accepts exactly one strict,
|
||||
bounded JSON bundle on standard input and requires the destination
|
||||
organization twice: once as `--organization-id` and again as the exact
|
||||
`--approve-organization` value. Import reparses every query, validates each
|
||||
resource scope against the destination organization, replaces all portable
|
||||
identifiers with cryptographically generated local identities, and commits
|
||||
the queries, dashboard, and panels in one SQLite transaction. A bundle cannot
|
||||
select its tenant or actor.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
observatory export --organization-id organization-id \
|
||||
--actor-user-id user-id --dashboard operations >operations.json
|
||||
|
||||
observatory import --organization-id organization-id \
|
||||
--approve-organization organization-id --actor-user-id user-id \
|
||||
<operations.json
|
||||
```
|
||||
|
||||
The Sandwich Hime interface creates saved queries and one-panel dashboards
|
||||
through organization-authorized, CSRF-protected forms. A first assisted builder
|
||||
serializes fixed, bounded controls into ordinary text and then uses the same
|
||||
parser and stored AST contract as the text editor. It never sends a caller-
|
||||
constructed AST around query validation.
|
||||
|
||||
An authorized dashboard page exposes ordinary server-rendered forms for its
|
||||
metadata and panels. Metadata changes, panel additions, panel edits, and panel
|
||||
removals submit the dashboard's opaque identity and current revision. The
|
||||
server reloads the organization-owned dashboard, preserves every untouched
|
||||
panel, validates saved-query ownership and presentation compatibility, and
|
||||
uses the storage transaction's optimistic revision check. A concurrent edit
|
||||
returns `409 Conflict` and requires a reload; it never silently overwrites the
|
||||
newer definition. These controls remain fully usable without JavaScript.
|
||||
|
||||
Dashboard pages execute every saved panel through the same bounded query
|
||||
engine and retain the full table for every presentation. Time-series panels
|
||||
may add a native `meter` summary for at most 48 finite, nonnegative numeric
|
||||
points; unsupported values fail closed to the table alone. Strict JSON export
|
||||
exposes only the source-control definition. The first preview intentionally
|
||||
keeps assisted editing to bounded controls and leaves richer expressions to
|
||||
the complete typed query editor; a second browser-side query language is not a
|
||||
release requirement.
|
||||
@@ -0,0 +1,305 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Tend deployment boundary
|
||||
|
||||
Observatory pins Tend `v0.2.0-preview.2` as a checksum-verified Go tool and uses
|
||||
its schema-2 singleton-candidate strategy on Linux. The
|
||||
release configuration is [`release/tend.json`](../release/tend.json). It pins
|
||||
the public origin, live and candidate loopback addresses, health paths,
|
||||
application marker, release root, state path, shared host lock, and exact build
|
||||
package. It contains no secret values.
|
||||
|
||||
## Two deliberately different processes
|
||||
|
||||
The installed service explicitly runs:
|
||||
|
||||
```text
|
||||
observatory server --config %d/server.json --systemd-credential-config
|
||||
```
|
||||
|
||||
Systemd copies the root-owned mode-`0600` configuration and Web Push private
|
||||
key into the service's private credential directory. Systemd 255 presents each
|
||||
runtime copy as a root-owned mode-`0440` credential confined to the unit;
|
||||
Observatory accepts that mode only when explicit systemd-credential validation
|
||||
also proves the credential-directory boundary and owner. The static
|
||||
`gamertan-observatory` service user can read those copies and write only its
|
||||
state directory. The originals stay root-owned mode `0600` under
|
||||
`/etc/gamertan-observatory/`.
|
||||
|
||||
Local operator commands that read the server configuration use the same
|
||||
explicit `--systemd-credential-config` flag and confined credential-directory
|
||||
policy. This applies to bootstrap, resource and enrollment management,
|
||||
retention and schema review, local queries, dashboard import/export, and
|
||||
migrations; no operator workflow needs a less restrictive duplicate config.
|
||||
|
||||
Tend executes a candidate binary without arguments and supplies only
|
||||
`OBSERVATORY_TEND_CANDIDATE_LISTEN=127.0.0.1:18093`. That narrowly named mode:
|
||||
|
||||
- accepts only a numeric loopback address and nonzero port;
|
||||
- exposes only `/`, `/healthz`, and `/readyz` over plain loopback HTTP;
|
||||
- returns a fixed marker and security headers;
|
||||
- does not read configuration, open storage, acquire a process lock, run a
|
||||
migration, evaluate alerts, send Web Push, access the network, or write a
|
||||
file.
|
||||
|
||||
The candidate proves that the exact binary starts and answers its bounded
|
||||
contract. It does not pretend to prove a live data migration. After the
|
||||
candidate passes, Tend stops it, atomically selects the immutable release,
|
||||
restarts the installed service, probes the real loopback application, and
|
||||
probes the routed HTTPS origin. A failure restores the previous release.
|
||||
|
||||
The candidate listen variable must never appear in the shared Tend environment
|
||||
file. Tend's server policy rejects that mistake. The live address remains in
|
||||
the root-owned Observatory configuration.
|
||||
|
||||
## Installed files
|
||||
|
||||
The reviewed deployment inputs are:
|
||||
|
||||
- `release/observatory.service`: hardened non-root service;
|
||||
- `release/server.json`: non-secret dogfood server configuration;
|
||||
- `release/observatory.env.example`: intentionally empty shared environment;
|
||||
- `release/Caddyfile.observatory`: initial origin handler;
|
||||
- `release/tend.json`: application-owned packaging and activation contract.
|
||||
|
||||
The Caddy handler also defines a bounded, rotated JSON edge log. It strips the
|
||||
entire query, request and response header maps, client addresses, and Caddy
|
||||
user identity before writing. A late `log_append` copies only the bounded
|
||||
application response request ID into a dedicated top-level field, allowing the
|
||||
agent to correlate accepted edge and application records without retaining
|
||||
cookies, credentials, referrers, user agents, or query values. The log file is
|
||||
not an application authorization record and remains separate evidence.
|
||||
|
||||
That installed handler is the privacy-minimized deployment policy, not a
|
||||
platform prohibition. Applications that disclose and require richer traffic
|
||||
evidence can use per-source `sensitive_fields` with a separately reviewed Caddy
|
||||
filter, such as `examples/Caddyfile.sensitive-access-log`. The richer policy
|
||||
still removes both header maps and never retains cookies or authorization
|
||||
headers. Its query-key deny list is only a starting point: the application must
|
||||
add every secret-bearing parameter it accepts before enabling query collection.
|
||||
|
||||
On the host, their controlled destinations are:
|
||||
|
||||
```text
|
||||
/etc/gamertan-observatory/server.json root:root 0600
|
||||
/etc/gamertan-observatory/web-push.json root:root 0600
|
||||
/etc/tend/environment/observatory.env root:root 0600
|
||||
/etc/tend/services/observatory.json root:root 0644
|
||||
/etc/systemd/system/gamertan-observatory.service
|
||||
/opt/gamertan-observatory/
|
||||
/var/lib/gamertan-observatory/
|
||||
```
|
||||
|
||||
The initial Caddy stanza is an explicit infrastructure change performed before
|
||||
the first Tend activation. Tend validates and uses the existing routed origin;
|
||||
it does not silently edit the host's top-level Caddyfile for a new service.
|
||||
|
||||
## First installation and the first Tend maintenance release
|
||||
|
||||
Tend intentionally bootstraps singleton state from an existing, validated
|
||||
`current` release pointer. It does not invent a service account, Caddy origin,
|
||||
root-owned configuration, or first live release. Observatory's one-time
|
||||
installation therefore uses a twice-built, checksum-approved Tend package from
|
||||
an exact clean pushed commit, exercises that binary in the stateless candidate
|
||||
mode, installs its immutable release, creates the first `current` pointer, and
|
||||
starts the reviewed service and Caddy origin under direct operator control.
|
||||
|
||||
The next exact pushed commit became the real Tend maintenance campaign:
|
||||
candidate health and application smoke, pointer activation, service restart,
|
||||
routed public-origin smoke, state recording, rollback, and identical-artifact
|
||||
redeployment. The observation below separates the one-time bootstrap from that
|
||||
completed dogfood exercise.
|
||||
|
||||
## Initial dogfood bootstrap observation
|
||||
|
||||
On August 17, 2026, the initial live Observatory bootstrap used a Tend package
|
||||
built twice from one clean pushed development source. Both package runs
|
||||
produced the same SHA-256 digest:
|
||||
|
||||
```text
|
||||
42c21deba0519955ea7cbb140ea37321403cb26622d54d69c68bcfd19fe2764e
|
||||
```
|
||||
|
||||
The installed `v0.0.1-preview.3` development binary reports Go 1.26.6. Tend
|
||||
validated and safely extracted the immutable archive, and the stateless
|
||||
candidate contract passed before the directly controlled first activation.
|
||||
The resulting non-root systemd service remained ready with zero restarts while
|
||||
the canonical HTTPS origin and the pre-existing EQL Helper, Gamertan, and
|
||||
Sandwich Hime origins were rechecked.
|
||||
|
||||
The bootstrap found two real integration defects before activation was
|
||||
accepted:
|
||||
|
||||
- systemd 255 presents a copied `LoadCredential` file as root-owned mode
|
||||
`0440`, although the original remains root-owned mode `0600`; Observatory
|
||||
originally rejected the runtime copy. The validator now permits `0440` only
|
||||
when explicit systemd-credential mode also proves that the file is inside
|
||||
the unit's credential directory and owned by root;
|
||||
- an omitted Web Push notifier was stored as a typed nil pointer in a non-nil
|
||||
interface, so the first incident evaluation panicked. Optional push
|
||||
construction now returns a genuinely nil interface when no notifier is
|
||||
configured, and its regression test exercises an incident with Web Push
|
||||
disabled.
|
||||
|
||||
Neither failure altered an existing application origin. Failed Observatory
|
||||
artifacts were retained as diagnostic evidence, and the service was accepted
|
||||
only after the corrected package passed its checks. Tend state remained
|
||||
intentionally uninitialized until the first maintenance activation.
|
||||
|
||||
## First Tend maintenance and rollback observation
|
||||
|
||||
Trusted CI run 220 passed the exact pushed maintenance source. Tend built it
|
||||
twice with Go 1.26.6 and module downloads disabled; both archives were
|
||||
byte-identical at:
|
||||
|
||||
```text
|
||||
3afed7dcc719d98d9bc9a4e13790ce0a71036c339f1a354fa4e0d67abab3ccdb
|
||||
```
|
||||
|
||||
On August 17, 2026, `tend push` sent that approved digest through the dedicated
|
||||
forced-command deployment account. The host policy accepted only the
|
||||
`observatory` service and validated the archive before Tend exercised the
|
||||
stateless candidate, activated `v0.0.1-preview.4`, restarted the installed
|
||||
unit, checked loopback health and readiness, probed the routed HTTPS origin,
|
||||
and initialized deployment state with `v0.0.1-preview.3` as the previous
|
||||
release.
|
||||
|
||||
The recorded Tend rollback then restored the exact preview.3 release and
|
||||
swapped preview.4 into the previous position. Loopback health, the public
|
||||
Observatory origin, and EQL Helper continuity passed before the same approved
|
||||
preview.4 archive was sent through the restricted transport again. Tend
|
||||
revalidated and reactivated its existing content-addressed release. Final
|
||||
state records preview.4 active and preview.3 previous.
|
||||
|
||||
The candidate port and shared host lock were free after the exercise. The
|
||||
service reported zero restarts and no warning-or-higher journal entries, and
|
||||
the Observatory, EQL Helper, Gamertan, and Sandwich Hime origins all returned
|
||||
success. This closes Observatory's initial Tend maintenance-and-rollback
|
||||
dogfood claim; it does not close the separate medium-fleet, capacity, or public
|
||||
preview release gates.
|
||||
|
||||
## Startup-readiness dogfood finding
|
||||
|
||||
On August 18, 2026, Tend correctly refused Observatory
|
||||
`v0.0.1-preview.13` after the activated process did not bind its loopback
|
||||
socket inside the 30-second post-activation window. Tend restored the exact
|
||||
preview.12 release, preserved the earlier rollback release, recorded the failed
|
||||
digest, and returned the service to active state with zero restarts.
|
||||
|
||||
The package and stateless candidate had passed. The full server then performed
|
||||
raw retention and identity-evidence pruning synchronously before opening its
|
||||
listener. Those maintenance passes are bounded and valid, but their elapsed
|
||||
time depends on accumulated data and cold filesystem state; they are not a safe
|
||||
availability prerequisite. Observatory now completes mandatory raw recovery,
|
||||
opens the configured listener, and only then runs retention and evidence
|
||||
pruning through the same background path used for hourly maintenance. A
|
||||
maintenance failure remains visible in bounded logs but does not prevent the
|
||||
otherwise healthy server from answering readiness probes.
|
||||
|
||||
This also records a Tend integration limitation: a stateless candidate proves
|
||||
the packaged binary and candidate process boundary, not a stateful
|
||||
application's complete startup path. Tend should continue to restore on a
|
||||
missed activation window, but a future release should report this distinction
|
||||
more explicitly and reject linked-worktree packaging before the build rather
|
||||
than after Go omits `vcs.revision`. The trusted Gitea release workflow produced
|
||||
and verified identical preview.13 bytes, but the Gitea Actions artifact API did
|
||||
not list the successfully uploaded artifact; the independently reproduced
|
||||
local digest and CI log therefore remained the review evidence for this
|
||||
attempt.
|
||||
|
||||
Preview.14 exposed a second, independent startup boundary on the accumulated
|
||||
production archive. Mandatory recovery enumerated every hot raw segment by
|
||||
checksum-verifying, decompressing, and retaining every decoded batch before it
|
||||
compared the archive with the control catalog. With 20,550 small segments this
|
||||
exceeded the service's 768 MiB memory limit before the listener opened. Tend
|
||||
restored the recorded release, but the same archive-bound algorithm also
|
||||
prevented that older binary from becoming ready; Tend therefore correctly
|
||||
left its bounded stateless candidate routed for operator recovery instead of
|
||||
claiming a successful rollback.
|
||||
|
||||
Recovery now walks only private filesystem metadata and compares each object
|
||||
with a prepared catalog lookup. It decodes one segment only when the catalog
|
||||
is missing that committed object, and it replays unprojected catalog rows in
|
||||
pages of 128. This fixes the availability defect rather than hiding it behind
|
||||
a larger memory allocation. Full checksum verification remains part of
|
||||
orphan admission, reads, cold archival, deletion, export, and explicit
|
||||
projection rebuild. A deployment with production-scale retained evidence must
|
||||
exercise the stateful startup path under its configured memory limit before
|
||||
activation; the stateless package candidate remains a separate gate.
|
||||
|
||||
## Preview.19 native-batch and stateful-Compose observation
|
||||
|
||||
Trusted Gitea release-candidate run 380 passed exact source commit
|
||||
`2b00be0984188b0c59d6a843921e03c9b182b881` with Go 1.26.6. Two independent
|
||||
Tend package runs and the retained CI artifact produced the same archive:
|
||||
|
||||
```text
|
||||
6ce16931412f618fa4a3d0130fc0f69e7f96f994b1f12e6be3a66a0e49a9af95
|
||||
```
|
||||
|
||||
The public-preview capacity gate passed at 221 observations/second sustained,
|
||||
1,315/second burst, approximately 60 ms visibility p95, approximately 2.1 ms
|
||||
query p95, and approximately 173 MiB maximum RSS. These are bounded synthetic
|
||||
gate results, not a medium-fleet or generic production-capacity claim.
|
||||
|
||||
Before activation, the exact candidate was exercised against a copy of the
|
||||
accumulated production control database, organization projection, and
|
||||
immutable raw archive. The first migration failed closed while building the
|
||||
new presence-only projection index: SQLite reported `database or disk is full`
|
||||
with the service's 64 MiB `/tmp` tmpfs. The same image, data, limits, and
|
||||
migration passed with a 512 MiB tmpfs, drained 986 pending raw segments to
|
||||
zero, and finished with 27,788 retained segments. The database was not corrupt;
|
||||
the index sort needed a larger bounded temporary working area. The reviewed
|
||||
Compose definition now records that scratch budget explicitly.
|
||||
|
||||
Observatory's production topology is a Docker Compose singleton, which Tend
|
||||
does not yet model. The maintainer therefore paused the durable agent, verified
|
||||
mode-`0600` SQLite backups and their SHA-256 digests, migrated the live data
|
||||
offline, and started the exact candidate under the staged Compose definition.
|
||||
Direct health and readiness passed approximately 3.25 seconds after the final
|
||||
start; the routed public origin settled approximately one second later.
|
||||
|
||||
The first activation script probed the public origin only once. That eager
|
||||
probe observed a transient `503` and invoked its rollback path even though the
|
||||
candidate had passed direct readiness. The rollback then demonstrated an
|
||||
important second boundary: Preview.18 correctly rejected the newly migrated
|
||||
control schema 11 and entered a fail-closed restart loop. The verified backups
|
||||
remained intact, but restoring them would have discarded valid forward
|
||||
migration work. The operator instead stopped the old agent, started the
|
||||
schema-compatible Preview.19 candidate, waited through bounded direct and
|
||||
public-origin readiness loops, and then started the matching Preview.19 agent.
|
||||
An independent observer recorded a bounded `502`/`503` interruption during
|
||||
the failed rollback and recovery. No continuous-delivery claim is made.
|
||||
|
||||
At the immediate post-activation check on August 19, 2026:
|
||||
|
||||
- the server and agent both reported the exact Preview.19 image, zero restarts,
|
||||
no OOM event, and no warning-or-higher log entry;
|
||||
- `/`, `/healthz`, and `/readyz` returned success through the public origin,
|
||||
while `/app/` returned its expected authenticated redirect;
|
||||
- `observatory check` returned `ok` against the live configuration and data;
|
||||
- all 27,998 retained segments were projected with zero pending work; and
|
||||
- the first stream advanced by the new agent had non-empty native-v2 batch and
|
||||
encoded-body digests, proving the authenticated batch-identity path in the
|
||||
live control catalogue.
|
||||
|
||||
This is staging dogfood evidence for an unreleased development build. It does
|
||||
not create a public tag, close the medium-fleet soak, or make the current
|
||||
Compose handoff a supported Tend strategy. It does prove that authenticated
|
||||
`(source, stream, sequence)` batch identity, exact digest replay protection,
|
||||
raw-first durability, and overlapping-time-window semantics work together on
|
||||
the live data plane without a global per-record deduplication index.
|
||||
|
||||
## Secrets and administration
|
||||
|
||||
No secret belongs in `tend.json`, the release manifest, process arguments, or
|
||||
the shared environment file. Web Push keys and later credentials use systemd
|
||||
credentials. Local operator bootstrap creates a one-time password in a new
|
||||
private file and is performed while the application is stopped or through a
|
||||
separately reviewed administrative procedure; it is not part of deployment
|
||||
activation. The first browser or API session can reach only rotation, logout,
|
||||
health, and immutable shell assets until the password is replaced.
|
||||
|
||||
The first dogfood deployment is an unreleased development build. A public tag
|
||||
requires the capacity campaign, rollback exercise, medium-fleet soak, reviewed
|
||||
public snapshot, checksums, SBOM, signatures, and release gates in the roadmap.
|
||||
@@ -0,0 +1,40 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Incidents and alert rules
|
||||
|
||||
Observatory alert rules reuse a stored, versioned query. They do not introduce
|
||||
a second expression language, arbitrary SQL, shell execution, or a remote
|
||||
plugin surface.
|
||||
|
||||
Each rule defines:
|
||||
|
||||
- one organization-owned saved query;
|
||||
- a bounded minimum result-row count;
|
||||
- one to ten required consecutive matching evaluations;
|
||||
- a severity and a 15-second to 24-hour evaluation interval; and
|
||||
- an enabled state.
|
||||
|
||||
The evaluator claims at most 64 due rules per pass. Every query retains the
|
||||
server's ordinary time, row, decoded-byte, and memory budgets. Rules execute
|
||||
without sensitive-field permission; a query that requires sensitive or
|
||||
unknown fields fails closed as `query_unavailable`. Query failure never clears
|
||||
an existing incident.
|
||||
|
||||
The durable lifecycle is:
|
||||
|
||||
1. The first matching evaluation opens a `pending` incident.
|
||||
2. The configured consecutive-match count promotes it to `firing`.
|
||||
3. An authorized responder may `acknowledge`, `silence`, or `resolve` it.
|
||||
4. A non-matching successful evaluation resolves the open incident.
|
||||
5. An expired silence returns to `firing` if the rule still matches.
|
||||
|
||||
Every transition appends a sequenced organization-scoped event. Events record
|
||||
only the transition, actor identifier, and time. They do not copy query text,
|
||||
query results, resource labels, or telemetry values into the control database.
|
||||
The SSE channel carries only a generic refresh hint.
|
||||
|
||||
PWA installation, explicit offline inbox access, application badge state, and
|
||||
generic Web Push are progressive layers over this durable lifecycle. Web Push
|
||||
is optional and best effort; it cannot create, update, resolve, or otherwise
|
||||
replace an incident record. Its exact privacy boundary is documented in
|
||||
[`PWA.md`](PWA.md).
|
||||
@@ -0,0 +1,76 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Sandwich Hime interface
|
||||
|
||||
Observatory's web interface is server-rendered with Hime-san
|
||||
`v1.0.0-beta.2`. Generated `.sando.go` files are committed and verified for
|
||||
two-pass byte and modification-time stability. The production binary links
|
||||
only `gamertan.com/sandwich-hime/sando@v1.0.0-beta.1`; the compiler does not
|
||||
enter the service dependency graph.
|
||||
|
||||
The current surface provides:
|
||||
|
||||
- a public project explanation;
|
||||
- local session sign-in and CSRF-protected sign-out;
|
||||
- selection among the authenticated user's active organizations;
|
||||
- bounded one-hour tables for logs, metrics, traces, and deployment evidence;
|
||||
- organization-authorized creation of persisted queries through typed text or
|
||||
the first assisted visual builder;
|
||||
- one-panel dashboards, bounded panel execution, nonnegative numeric meter
|
||||
summaries with complete table alternatives, and strict JSON export;
|
||||
- a progressive SSE notification when newer observations are available;
|
||||
- an organization-scoped incident inbox with durable lifecycle state and
|
||||
ordinary acknowledge, silence, and resolve forms; and
|
||||
- bounded alert-rule creation over an existing saved query;
|
||||
- an installable manifest, deterministic service worker, and offline public
|
||||
shell; and
|
||||
- explicit opt-in storage of a read-only incident snapshot with badge state.
|
||||
|
||||
Every organization selection is checked against both active membership and
|
||||
the scoped `dashboards.read` grant. The query executor receives the authorized
|
||||
organization separately from query text. No browser field can select a tenant
|
||||
without that server-side decision.
|
||||
|
||||
The SSE stream is deliberately an invalidation hint rather than a telemetry
|
||||
transport. It emits fixed `ready` and `refresh` events with `{}` payloads,
|
||||
coalesces pending refreshes, caps total and per-organization subscribers, and
|
||||
is authorized independently when the connection opens. If JavaScript is
|
||||
disabled, disconnected, or blocked, the same HTML tables and ordinary refresh
|
||||
links remain usable.
|
||||
|
||||
CSS and JavaScript are embedded, content-addressed, and served with immutable
|
||||
cache policy. The application CSP permits only same-origin styles, scripts,
|
||||
images, forms, and SSE connections; it does not use `unsafe-inline`.
|
||||
|
||||
The assisted builder deliberately produces one optional comparison and one
|
||||
optional aggregate/group/bucket stage from fixed controls. It serializes that
|
||||
selection into ordinary query text and submits it to the same parser used by
|
||||
the text editor; it does not maintain a second query language. More complex
|
||||
queries remain available through typed text.
|
||||
|
||||
Time-series panels render at most 48 finite, nonnegative points with native
|
||||
`meter` elements. The complete result table is always rendered beside the
|
||||
summary and remains the canonical accessible representation. Negative,
|
||||
non-numeric, missing, or unsupported results simply omit the visual summary.
|
||||
|
||||
Alert evaluation counts bounded saved-query rows, requires a configured number
|
||||
of consecutive matches before promotion from pending to firing, and never
|
||||
resolves an existing incident when query execution itself fails. The incident
|
||||
event trail records system transitions and authorized human responses without
|
||||
copying query results or telemetry values into control state.
|
||||
|
||||
The service worker precaches only public content-addressed shell assets. It
|
||||
stores no private response by default. An authorized user must activate the
|
||||
offline control before the worker fetches a separate read-only incident page
|
||||
and stores it under that organization's ordinary inbox URL. The snapshot has
|
||||
no actions, CSRF material, query text, telemetry values, actors, or project,
|
||||
environment, and service identifiers. Signing out asks the worker to delete
|
||||
the complete private cache. Session expiry cannot itself reach an offline
|
||||
browser, so this remains an explicit device-local privacy decision.
|
||||
|
||||
Dashboard metadata and panel editing use optimistic revisions, and the bounded
|
||||
assisted query builder shares the same parser as the complete typed editor.
|
||||
The incident inbox provides an installable shell, explicit read-only offline
|
||||
copy, badge state, and optional generic Web Push without making JavaScript an
|
||||
authority for incident response. Richer visual editing remains future work,
|
||||
not an alternate query authority or a hidden first-preview gate.
|
||||
@@ -0,0 +1,232 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Performance engineering ledger
|
||||
|
||||
This document preserves measured optimization work, including changes that
|
||||
were deliberately rejected. It prevents an attractive idea from being
|
||||
reimplemented and rerun without a materially different premise.
|
||||
|
||||
The capacity fixture is a synthetic engineering instrument. Its local results
|
||||
are useful for comparing two exact implementations on the same host; they are
|
||||
not production sizing claims and do not replace the constrained release
|
||||
campaign in [CAPACITY.md](CAPACITY.md).
|
||||
|
||||
## Current data path
|
||||
|
||||
Observatory deliberately separates four jobs:
|
||||
|
||||
1. the agent batches typed observations;
|
||||
2. the server validates and durably acknowledges an immutable, checksummed,
|
||||
zstd-compressed raw segment;
|
||||
3. a background projector builds disposable per-organization SQLite read
|
||||
models, typed indexes, and rollups; and
|
||||
4. queries read authorized projections or budgeted cold evidence.
|
||||
|
||||
Raw segments are the forensic truth. SQLite is currently a replaceable query
|
||||
accelerator, not the only copy of an observation. A bounded in-memory layer may
|
||||
accelerate the recent live window, but it must populate only after raw
|
||||
acknowledgement and must remain safe to lose and rebuild.
|
||||
|
||||
## Experiment ledger
|
||||
|
||||
All August 18 local comparisons used the same Linux/WSL2 checkout based on
|
||||
private commit `7c4f834aee8afcb2e9c3ae534b1b37b03259c9a3`, Go 1.26.6, the same synthetic
|
||||
mixed workload, and an unconstrained 16-logical-CPU host. They are comparative
|
||||
diagnostics, not release evidence.
|
||||
|
||||
| Experiment | Corpus | Sustain / burst | Visibility p95 | Fill or query notes | Result |
|
||||
| --- | ---: | ---: | ---: | --- | --- |
|
||||
| Existing prepared single-row projection writes | 100,000 | 2,630.64/s / 13,084.50/s | 84.7 ms / 228.1 ms | about 198 MB peak RSS; about 176.9 MB dataset | Baseline retained. |
|
||||
| 256-row multi-value SQLite inserts | 100,000 | 2,631.28/s / 13,015.34/s | 200.7 ms / 382.3 ms | about 197 MB peak RSS; about 205.6 MB dataset | Rejected and removed: no throughput gain, worse visibility and storage. |
|
||||
| `synchronous=NORMAL` projection plus transaction receipts and recovery reconciliation | 100,000 | 2,630.13/s / 12,991.85/s | 175.35 ms / 230.76 ms | 199,081,984-byte peak RSS; 177,218,281-byte dataset | Rejected and removed: added recovery machinery without a measurable throughput gain. |
|
||||
| Existing prepared single-row projection writes | 1,000,000 | 3,815.33/s / 18,901.41/s | 121.83 ms / 257.26 ms | fill 6.604 s; queries 6.226/1.839/1.557 ms; 1,090,726,860-byte dataset | Larger baseline retained. |
|
||||
| `synchronous=NORMAL` projection plus receipts | 1,000,000 | 3,823.49/s / 18,766.63/s | 173.67 ms / 219.41 ms | fill 6.789 s; queries 6.637/1.721/1.586 ms; 1,092,327,298-byte dataset | Rejected and removed: within noise on throughput, slower fill, larger dataset. |
|
||||
|
||||
Do not repeat either rejected experiment unless the transaction shape, schema,
|
||||
SQLite version, storage medium, or workload has materially changed. Record the
|
||||
new premise and a same-host baseline before doing so.
|
||||
|
||||
## Native v2 exact-replay envelope
|
||||
|
||||
The native v2 endpoint places bounded batch identity metadata in authenticated
|
||||
request headers before the JSON record body. A retry of the current
|
||||
acknowledged `(source, stream, sequence)` can therefore hash and discard the
|
||||
complete bounded body, recheck the watermark under the source lock, and return
|
||||
the retained acknowledgement without JSON decoding, zstd compression,
|
||||
catalogue writes, or projection notification. New batches still follow the
|
||||
complete validation and raw-first transaction.
|
||||
|
||||
An August 18, 2026 Linux/amd64 Go 1.26.6 HTTP-handler microbenchmark used an
|
||||
exact 500-record, approximately 60.6-KB JSON retry on an Intel i7-10700K. Five
|
||||
independent benchmark samples recorded:
|
||||
|
||||
| Path | Median time | Bytes allocated/op | Allocations/op |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Legacy native v1 exact retry | 4.105 ms | about 22.4 MB | about 14,760 |
|
||||
| Framed native v2 exact retry | 0.275 ms | about 49.9 KB | 301 |
|
||||
|
||||
For this one retry workload, v2 used about 14.9 times less wall time, 449 times
|
||||
fewer allocated bytes, and 49 times fewer allocations. Reproduce with:
|
||||
|
||||
```sh
|
||||
go test -buildvcs=false ./internal/httpserver \
|
||||
-run '^$' -bench '^BenchmarkNativeExactReplay$' -benchmem -count=5
|
||||
```
|
||||
|
||||
This is not new-batch throughput, whole-agent delivery latency, or a production
|
||||
capacity result. It proves only that the early exact-replay path removes the
|
||||
intended duplicate work. The server still reads and hashes every replay byte;
|
||||
transport integrity and the immutable retained segment remain authoritative.
|
||||
|
||||
## First cost-centre diagnostic
|
||||
|
||||
The capacity report now records projection backlog/drain time, raw/control/
|
||||
projection file bytes, and SQLite page use by schema object. The fields contain
|
||||
only aggregate counts and checked-in schema names; they contain no telemetry
|
||||
values, tenant identifiers, or local paths.
|
||||
|
||||
An unconstrained local 100,000-primary/109,000-total observation diagnostic on
|
||||
the same August 18 baseline recorded:
|
||||
|
||||
- 19 pending segments and 14,936,883 decoded bytes at the start of drain;
|
||||
- 4.580 seconds to drain that backlog;
|
||||
- 446,217 bytes of compressed raw segments;
|
||||
- 158,247,552 bytes across all organization projection SQLite files and
|
||||
sidecars;
|
||||
- 1,705,496 bytes of control SQLite files and sidecars; and
|
||||
- 97,124,352 allocated SQLite page bytes in the primary projection, comprising
|
||||
28,409,856 table bytes, 68,702,208 index bytes, and 12,288 internal bytes.
|
||||
|
||||
The synthetic observations compress unusually well, so the roughly 355:1
|
||||
projection-to-compressed-raw ratio must not be extrapolated to real telemetry.
|
||||
The within-SQLite result is still actionable: indexes consumed about 2.42
|
||||
times the table bytes and about 70.7 percent of allocated primary-projection
|
||||
pages. Index write amplification is therefore the first implementation case
|
||||
study. This one short diagnostic is not a release capacity result.
|
||||
|
||||
## Presence-only base-index candidate
|
||||
|
||||
The first index case study replaced four full projection indexes with partial
|
||||
indexes that contain only observations where the indexed value is present:
|
||||
metric value, HTTP route, HTTP status, and request duration. Severity remains
|
||||
a full index because its current storage representation uses an empty string
|
||||
for absence. The migration is independently versioned and changes the old and
|
||||
new index sets in one SQLite transaction; an interrupted migration therefore
|
||||
retains the complete prior set and safely retries.
|
||||
|
||||
Same-host comparisons against the cost-centre baseline recorded:
|
||||
|
||||
| Corpus | Baseline index bytes | Candidate index bytes | All projection SQLite | Projection drain | Query p95 notes |
|
||||
| ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| 100,000 primary observations | 68,702,208 | 57,135,104 (-16.8%) | 158,247,552 to 143,429,744 (-9.4%) | 4.580 s to 4.704 s | Same millisecond range; drain difference is within short-run noise. |
|
||||
| 1,000,000 primary observations | 693,755,904 | 572,768,256 (-17.4%) | 1,087,731,688 to 954,606,472 (-12.2%) | 58.267 s to 54.535 s (-6.4%) | Baseline 6.157/1.908/1.659 ms; candidate 6.156/5.444/1.680 ms. The isolated recent-items increase remains only milliseconds and needs repetition before interpretation. |
|
||||
|
||||
The million-observation candidate retained the identical 283,639,808 table
|
||||
bytes and reduced the primary projection from 977,408,000 to 856,420,352
|
||||
allocated bytes. Synthetic compressed raw sizes were 4,011,063 and 4,031,350
|
||||
bytes respectively; that small corpus variance is not attributed to the index
|
||||
change.
|
||||
|
||||
This is a promising bounded schema improvement, not proof that continuous
|
||||
universal projection is the final architecture. Migration and selective-query
|
||||
tests must pass from legacy preview databases, followed by the full verifier
|
||||
and trusted CI, before adoption. The adaptive raw-first design remains the
|
||||
larger direction.
|
||||
|
||||
## What the evidence says
|
||||
|
||||
SQLite is not yet proven to be the limiting technology. The failed
|
||||
ten-million-observation campaign proves that the current *projection shape*
|
||||
cannot drain the synthetic fill inside the published window. It does not
|
||||
separate SQLite's engine cost from Observatory's choices around row shape,
|
||||
index count, JSON expression indexes, typed descriptor indexing, rollups, WAL
|
||||
checkpoints, or single-organization serialization.
|
||||
|
||||
The two rejected experiments also show that transaction syntax and one
|
||||
durability pragma are not the dominant cost. The next work must measure cost
|
||||
centres instead of swapping databases or adding cache infrastructure by
|
||||
intuition.
|
||||
|
||||
## Required cost-centre evidence
|
||||
|
||||
Add aggregate, value-free timing and byte counters to the synthetic fixture
|
||||
for:
|
||||
|
||||
- native batch validation, JSON encoding, zstd compression, file write,
|
||||
file `fsync`, directory `fsync`, and control-catalog commit;
|
||||
- raw-segment open, checksum, decompression, and decode;
|
||||
- base observation insertion, built-in index maintenance, reviewed custom
|
||||
index maintenance, metric rollups, log rollups, WAL commit, and checkpoint;
|
||||
- projector queue depth, oldest lag, rows and bytes per transaction, active
|
||||
writer time, and time waiting for an organization lock; and
|
||||
- query planning, rows/bytes scanned, SQLite execution, cold decode, and result
|
||||
encoding.
|
||||
|
||||
These counters belong only in the capacity fixture or bounded internal
|
||||
instrumentation. They must not include telemetry values, local paths,
|
||||
credentials, host names, or tenant identifiers.
|
||||
|
||||
Projection backlog/drain and file/page accounting above are implemented. The
|
||||
per-operation CPU/wall-time breakdown remains open.
|
||||
|
||||
## Ordered technical case studies
|
||||
|
||||
The larger lazy/eager partition design is specified in
|
||||
[ADAPTIVE_PROJECTIONS.md](ADAPTIVE_PROJECTIONS.md). The ordered experiments
|
||||
below are its evidence path, not independent promises to ship every idea.
|
||||
|
||||
### 1. Index write amplification
|
||||
|
||||
The base observation table currently maintains its primary key plus general
|
||||
scope/time/name indexes and signal-specific severity, value, correlation, HTTP
|
||||
JSON-expression, trace, and span access paths. Measure per-index bytes and
|
||||
projection time. Compare only reviewed alternatives such as partial
|
||||
signal-specific indexes or replacing a generic expression index with an exact
|
||||
rollup already used by the supported query shape. Every candidate must rerun
|
||||
the existing query and cold-evidence gates.
|
||||
|
||||
### 2. Time and signal partitioning
|
||||
|
||||
One organization currently has one SQLite writer. A partition per bounded time
|
||||
window and/or signal could let logs, metrics, and traces project concurrently,
|
||||
limit index working sets, and make retention a file-level operation. The cost
|
||||
is a more complex planner, bounded multi-database queries, atomic partition
|
||||
catalogue changes, and additional recovery cases. Prototype this only after
|
||||
the index-cost campaign identifies single-database write amplification.
|
||||
|
||||
### 3. Bounded recent-memory view
|
||||
|
||||
A per-organization ring keyed by segment digest can make the latest live tail
|
||||
visible immediately after durable raw acknowledgement while disk projection
|
||||
continues. Queries would merge the bounded memory window with disk results and
|
||||
deduplicate by segment/record identity. This improves live experience and
|
||||
absorbs short projection bursts; it does not increase durable projector
|
||||
throughput and must never become acknowledgement truth.
|
||||
|
||||
### 4. Durable admission group commit
|
||||
|
||||
Each accepted native batch currently creates and synchronizes its own raw file
|
||||
and directory, then commits the control catalogue. A bounded group-commit
|
||||
coordinator could acknowledge several independent streams after one directory
|
||||
sync and one control transaction. It must preserve per-stream ordering,
|
||||
organization quota checks, exact digest acknowledgements, cancellation, and a
|
||||
small maximum wait. This is relevant only if cost-centre evidence places the
|
||||
limit on durable admission rather than projection.
|
||||
|
||||
### 5. Projection representation
|
||||
|
||||
If row/index tuning and partitioning remain insufficient, compare a compact
|
||||
typed projection representation against SQLite on identical raw segments.
|
||||
Candidates must retain authorization-before-open, bounded queries, atomic
|
||||
replacement, offline rebuild, corruption handling, and small-server operation.
|
||||
An external database is not an optimization if it merely moves the same write
|
||||
amplification into more infrastructure.
|
||||
|
||||
## Database decision boundary
|
||||
|
||||
Keep SQLite while it satisfies the measured small-server boundary with simpler
|
||||
operations. Consider another embedded or external engine only after an exact
|
||||
candidate demonstrates that the required workload remains blocked by SQLite
|
||||
itself after index, partition, and batching work. Any replacement must beat the
|
||||
same fixture while preserving tenant isolation, raw replay, deterministic
|
||||
recovery, query budgets, deployment simplicity, and idle resource use.
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Public snapshot boundary
|
||||
|
||||
Observatory uses a private development repository and a separate canonical
|
||||
public source repository. Public history is not a mirror of private history.
|
||||
Each publication is a reviewed root snapshot of one exact, clean, pushed
|
||||
private `main` commit.
|
||||
|
||||
`scripts/public-snapshot.allow` is the complete public file boundary. The
|
||||
exporter refuses workflow directories, absolute or parent-traversing paths,
|
||||
missing tracked files, dirty worktrees, and a private HEAD that differs from
|
||||
`origin/main`. It archives only the allowlisted paths from the committed Git
|
||||
tree, byte-compares them with the worktree, scans the result for known private
|
||||
material, and records the source commit, tree, date, and file count in
|
||||
`PUBLIC-SNAPSHOT.json` with a SHA-256 sidecar.
|
||||
|
||||
The generated snapshot has no `.git` directory. Publication tooling creates a
|
||||
new reviewed public commit; it must never push private refs, tags, workflows,
|
||||
reflogs, or historical objects. The canonical public Gitea commit and GitHub
|
||||
discovery commit may have different Git identities, but their exported file
|
||||
trees and snapshot manifest must be byte-identical.
|
||||
|
||||
Before the first preview, the public snapshot, release archive, SBOM,
|
||||
checksums, signature, canonical tag, and installed module must all be traced
|
||||
back to the same reviewed private source commit. A public snapshot does not by
|
||||
itself make an unreleased development build supported.
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Progressive web application boundary
|
||||
|
||||
Observatory's PWA support adds availability without turning browser storage
|
||||
into an implicit telemetry replica.
|
||||
|
||||
The deterministic service worker precaches only:
|
||||
|
||||
- the public offline explanation;
|
||||
- content-addressed CSS and JavaScript; and
|
||||
- the content-addressed application icon.
|
||||
|
||||
It does not automatically cache authenticated navigation responses. On the
|
||||
incident page, an authorized user may explicitly save a separate read-only
|
||||
snapshot. The worker validates both same-origin URLs, requires one matching
|
||||
organization selector, fetches with the current session, verifies a successful
|
||||
HTML response, and stores that response under the ordinary inbox URL.
|
||||
|
||||
The saved page contains the organization name plus current incident titles,
|
||||
states, severities, and times. It excludes incident identifiers, response
|
||||
forms, CSRF tokens, saved-query text, telemetry values, actor identifiers, and
|
||||
project, environment, and service identifiers. The page is not an authority
|
||||
for response actions; reconnection is required.
|
||||
|
||||
Signing out sends a private-cache deletion request before submitting the
|
||||
ordinary sign-out form. An expired server session cannot notify a disconnected
|
||||
browser, so operators should treat saved incident snapshots like any other
|
||||
explicitly downloaded sensitive material on that device.
|
||||
|
||||
The Badging API receives only the current open-incident count when supported.
|
||||
|
||||
## Optional Web Push
|
||||
|
||||
Web Push follows [RFC 8291](https://www.rfc-editor.org/rfc/rfc8291.html) message
|
||||
encryption and [RFC 8292](https://www.rfc-editor.org/rfc/rfc8292.html) VAPID
|
||||
authentication. It is disabled unless the server configuration names a VAPID
|
||||
private-key file, contact subject, bounded queue, and request timeout. Create a
|
||||
new private key without exposing it in command arguments or output:
|
||||
|
||||
```sh
|
||||
observatory admin web-push generate-key \
|
||||
--output-file /etc/gamertan-observatory/web-push.json
|
||||
```
|
||||
|
||||
The output is created exclusively as a mode-`0600` regular file. The installed
|
||||
systemd unit copies that root-owned source into its private credential
|
||||
directory as `web-push.json`, and the non-root server configuration references
|
||||
only that runtime copy. Do not place the key value in configuration, process
|
||||
arguments, deployment manifests, or source control:
|
||||
|
||||
```json
|
||||
"web_push": {
|
||||
"private_key_file": "/run/credentials/gamertan-observatory.service/web-push.json",
|
||||
"subject": "mailto:security@sandwichhime.com",
|
||||
"queue_capacity": 64,
|
||||
"request_timeout": "10s"
|
||||
}
|
||||
```
|
||||
|
||||
The source key remains `/etc/gamertan-observatory/web-push.json`, owned by
|
||||
root with mode `0600`. `LoadCredential=` makes the service-specific runtime
|
||||
copy readable without granting the static service account access to `/etc` or
|
||||
weakening the original file. A differently named service must update both the
|
||||
unit and the credential-directory path explicitly; Observatory does not search
|
||||
for secrets.
|
||||
|
||||
An authorized incident reader must press the browser control before
|
||||
Observatory requests notification permission or creates a subscription.
|
||||
One browser endpoint is owned by one user and may be mapped to several of that
|
||||
user's organizations. Removing one organization keeps the browser subscribed
|
||||
until its final mapping is removed. Authorization is checked again for every
|
||||
delivery; revoked access retires only that organization's mapping.
|
||||
Endpoints are accepted only as bounded HTTPS push-service URLs and delivery
|
||||
uses a redirect-free client that rejects DNS results containing non-public IP
|
||||
addresses.
|
||||
|
||||
Only a transition into `firing` enqueues a notification. The bounded queue is
|
||||
best effort and never blocks alert evaluation or incident persistence. The
|
||||
encrypted payload is always exactly:
|
||||
|
||||
> Gamertan Observatory needs your attention.
|
||||
|
||||
The service worker ignores incoming payload content and renders that same
|
||||
fixed sentence. It includes no organization, host, service, severity, rule,
|
||||
incident identifier, count, or telemetry text. Activating it opens `/app/`,
|
||||
where the user must have a valid authenticated session before seeing details.
|
||||
Browser-vendor push relays remain an explicit metadata boundary: they can
|
||||
observe delivery timing, endpoint identity, and the fixed payload size even
|
||||
though they cannot read its encrypted content.
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Query execution
|
||||
|
||||
Observatory's text editor and assisted visual builder produce the same
|
||||
versioned typed AST. The builder serializes its bounded controls to reviewable
|
||||
query text and submits that text to the ordinary parser; it does not create a
|
||||
parallel query language or bypass validation. Query text never selects or
|
||||
overrides an organization: the HTTP server and local CLI authorize the
|
||||
requested resource scope separately before planning or opening its projection.
|
||||
|
||||
```text
|
||||
logs
|
||||
| where service == "eql"
|
||||
| where status >= 500
|
||||
```
|
||||
|
||||
Use one `where` stage per comparison in the current preview:
|
||||
|
||||
```text
|
||||
logs
|
||||
| where service == "eql"
|
||||
| where status >= 500
|
||||
| window 24h
|
||||
| summarize count(), p95(duration) by route, window(5m)
|
||||
| sort count desc
|
||||
| limit 50
|
||||
```
|
||||
|
||||
`window 24h` limits the lookback range. `window(5m)` creates five-minute
|
||||
summary buckets; these are separate AST fields. Filters are typed from reviewed
|
||||
field descriptors. Regular expressions use Go's bounded RE2 implementation,
|
||||
not a backtracking engine. Arbitrary SQL is never accepted.
|
||||
|
||||
The first assisted builder covers one optional comparison, one lookback, one
|
||||
optional aggregate, one optional grouping field, one optional time bucket, and
|
||||
a fixed row limit. Values are quoted before serialization and a literal
|
||||
pipeline character is escaped before parsing, so form values cannot introduce
|
||||
new stages. Multi-filter and multi-aggregate work remains in the text editor
|
||||
until a richer builder can preserve the same explicit AST contract.
|
||||
|
||||
Reviewed custom fields use the active per-organization descriptor registry.
|
||||
An exact or range filter on an activated indexed field is correlated through
|
||||
that version's typed index instead of casting arbitrary raw JSON. Values that
|
||||
did not satisfy the reviewed type during the index build remain in raw truth
|
||||
but do not silently become zero or another valid indexed value.
|
||||
|
||||
Unknown fields remain raw-queryable only to principals with the separate
|
||||
sensitive-telemetry permission. Results contain fixed safe identity columns
|
||||
plus fields explicitly referenced by the query; sensitive bodies are never a
|
||||
default result column.
|
||||
|
||||
Every execution has independent duration, row, projected-byte, and memory
|
||||
limits. `explain` reports the authorized projection, descriptors, index policy,
|
||||
conservative scan estimate, cache eligibility, permissions, and budgets before
|
||||
execution.
|
||||
|
||||
## Storage scale is not query cost
|
||||
|
||||
Observatory deliberately separates admission of evidence from execution of a
|
||||
query. A large organization store, historical import, or forensic archive is
|
||||
not itself a reason to reject new data or a small recent query. Ingestion and
|
||||
import use their own explicit storage quotas, bounded batches, backpressure,
|
||||
durable progress, and capacity checks. Query limits never act as an implicit
|
||||
organization-size quota.
|
||||
|
||||
Planning considers the authorized signal, resource scope, time window,
|
||||
available indexes or rollups, and result limit. A projection file's total size
|
||||
alone must not make an indexed, bounded recent-record query unavailable. The
|
||||
executor still enforces actual duration, rows, logical bytes read, and memory;
|
||||
summaries, alternate sorts, regular-expression filters, and cold forensic
|
||||
reads retain conservative preflight because they may need to examine more of
|
||||
the selected evidence. `explain` should make that distinction visible rather
|
||||
than promising that a cheap result follows from a small output alone.
|
||||
|
||||
A log summary with one canonical `status >= N` threshold, `count()`, a route
|
||||
group, and either no bucket or a multiple-of-five-minute bucket uses an exact
|
||||
five-minute projection. Its explain source ends in
|
||||
`/rollup:http-status-route:5m`. The server still injects the organization and
|
||||
optional project, environment, and service scope; the same duration, scan,
|
||||
memory, and result limits apply. Missing routes remain distinct from explicitly
|
||||
empty routes, and malformed stored statuses are excluded rather than cast to
|
||||
zero. A lower time boundary inside a bucket reads only that raw partial bucket
|
||||
and merges it with the complete projected buckets. Queries outside that exact
|
||||
shape remain on the general executor instead of receiving a different
|
||||
interpretation merely to reach a faster plan.
|
||||
|
||||
Metric summaries with no per-sample value filter and a bucket of at least five
|
||||
minutes can use the retained aggregate projection. Its explain source ends in
|
||||
`/rollup:5m`. Counts, sums, minima, maxima, and averages remain exact;
|
||||
percentiles use the bounded rollup histogram and set
|
||||
`statistics.approximate=true`. Unknown, sensitive, high-cardinality, and
|
||||
raw-only fields make the query use raw samples instead of silently reading an
|
||||
incomplete rollup.
|
||||
|
||||
Hot projection expiry does not make older evidence disappear. A query whose
|
||||
lookback overlaps cold segments includes their catalogued uncompressed size in
|
||||
the explain estimate, verifies and decompresses each exact zstd object, and
|
||||
applies the same organization scope, sensitive-field permission, typed
|
||||
filters, and execution budgets. This path deliberately accepts more latency
|
||||
for forensic detail. If matching cold metric segments exist, Observatory uses
|
||||
the exact raw path rather than combining them with an incomplete rollup.
|
||||
The explain source adds `/cold:raw` when that tier participates.
|
||||
|
||||
## Local authorized query
|
||||
|
||||
The local command reads query text from standard input so values do not need to
|
||||
appear in process arguments. Local operating-system access does not grant
|
||||
telemetry access: `--actor-user-id` must still hold the scoped query grant.
|
||||
|
||||
```sh
|
||||
printf '%s\n' 'logs | where status >= 500 | window 1h | limit 50' |
|
||||
observatory query \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID
|
||||
```
|
||||
|
||||
Use the optional project, environment, and service flags to narrow the
|
||||
authorized scope. Output is a stable versioned JSON table: column descriptors
|
||||
are ordered once and each row contains positional nullable string values in the
|
||||
column's declared type and unit.
|
||||
|
||||
## HTTP endpoints
|
||||
|
||||
- `POST /api/v1/query/parse` validates text or builder AST input.
|
||||
- `POST /api/v1/query/explain` requires a scoped query grant.
|
||||
- `POST /api/v1/query` executes the same authorized plan.
|
||||
|
||||
Session-backed endpoints require the canonical same origin. Error responses do
|
||||
not echo query values or telemetry.
|
||||
@@ -0,0 +1,110 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Recovery and projection rebuild
|
||||
|
||||
Observatory treats immutable, checksummed raw segments as the replayable
|
||||
telemetry truth. Per-organization SQLite projections, custom indexes, query
|
||||
caches, and rollups are disposable products of that truth.
|
||||
|
||||
## Ordinary startup recovery
|
||||
|
||||
All three commands reconcile raw segments that reached durable storage before
|
||||
their control record. `observatory server` then opens its listener and drains
|
||||
catalogued but unprojected segments through the bounded background projector.
|
||||
`observatory check` and `observatory migrate` wait for that projection work as
|
||||
part of their offline verification. This closes both crash windows without
|
||||
making source acknowledgement or server readiness depend on disposable query
|
||||
state. Already projected segments are not rewritten.
|
||||
|
||||
Discovery walks only private raw-object metadata: organization, source,
|
||||
stream, sequence, path, encoded size, and the content digest carried in the
|
||||
immutable filename. Observatory compares already catalogued objects with the
|
||||
control database without reading their telemetry. Only an object missing from
|
||||
the catalog is checksum-verified, decompressed, decoded, validated, and
|
||||
admitted. Pending projections are replayed in bounded, organization-local
|
||||
groups. This keeps recovery memory proportional to one fixed directory batch,
|
||||
one bounded segment group, and one small projection page rather than to the
|
||||
entire retained archive.
|
||||
|
||||
That bounded startup path is not a substitute for forensic verification.
|
||||
Checksums are still verified when an orphan is admitted, a segment is queried,
|
||||
moved to cold storage, deleted, exported, or used for an explicit projection
|
||||
rebuild. Operators should schedule those evidence checks and backups rather
|
||||
than forcing every accepted raw object through memory before the listener can
|
||||
open.
|
||||
|
||||
When an explicit policy enables cold deletion, startup recovery also completes
|
||||
a segment retirement that was interrupted
|
||||
after its durable control-state transition. It removes any remaining raw
|
||||
projection rows, verifies and removes the exact checksummed segment, and only
|
||||
then clears its control record. A missing retired file is an idempotent state;
|
||||
a changed file or path is a hard failure.
|
||||
|
||||
Cold archival has an earlier independent recovery boundary. Observatory first
|
||||
records the exact cold path, then atomically renames the verified zstd object,
|
||||
then advances its catalog tier. Startup safely completes either a pending move
|
||||
or a move that reached disk before the catalog update. It will not follow a
|
||||
symlink, accept another path, or rebuild altered bytes.
|
||||
|
||||
The shipped policy never starts the retirement transition. Cold raw evidence
|
||||
therefore remains available until an organization or server operator has
|
||||
deliberately enabled `delete_cold_raw`; projection expiry and compaction do not
|
||||
silently imply evidence deletion.
|
||||
|
||||
To run retention and SQLite compaction explicitly while the server is stopped:
|
||||
|
||||
```sh
|
||||
observatory migrate \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--apply-retention
|
||||
```
|
||||
|
||||
The long-running server and ordinary local commands hold a shared lock on the
|
||||
data directory. Offline migration and projection replacement require an
|
||||
exclusive lock, so they fail closed while any participating Observatory
|
||||
process is using that directory.
|
||||
|
||||
## Rebuild one organization
|
||||
|
||||
Stop the Observatory server, retain a filesystem-level backup, and run:
|
||||
|
||||
```sh
|
||||
observatory migrate \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--rebuild-organization organization-id \
|
||||
--approve-rebuild-organization organization-id
|
||||
```
|
||||
|
||||
The two organization values must match exactly. The command:
|
||||
|
||||
1. acquires exclusive ownership of the data directory;
|
||||
2. refuses unknown organizations, symlinked projections, and unsafe SQLite
|
||||
sidecars;
|
||||
3. reads every registered **hot** raw segment for the organization through the
|
||||
checksum-verifying segment store; cold evidence remains outside the fast
|
||||
projection and available through the budgeted cold query path;
|
||||
4. rebuilds the base projection and the organization's activated descriptor
|
||||
version beside the live projection;
|
||||
5. finalizes the replacement as a private standalone SQLite file; and
|
||||
6. atomically replaces only that organization's projection and synchronizes
|
||||
its directory.
|
||||
|
||||
Validation or reconstruction failures before activation remove the temporary
|
||||
files and preserve the existing projection. Other organizations are not
|
||||
opened or rewritten. The JSON report contains the organization, raw segment
|
||||
and observation counts, active projection version, and indexed-row count; it
|
||||
contains no telemetry values.
|
||||
|
||||
## Evidence boundary
|
||||
|
||||
The rebuild proves that the currently registered checksummed hot segments can
|
||||
recreate the fast disposable projection. Cold segments remain independently
|
||||
checksummed forensic truth and are not silently promoted back into hot SQLite.
|
||||
Neither mechanism replaces backups of the control database, raw/cold segments,
|
||||
server configuration, identity state, or encryption keys. A missing or corrupt
|
||||
segment is a hard failure, not a reason to silently accept partial history.
|
||||
|
||||
No public Observatory preview exists yet, so there is not yet a supported
|
||||
cross-preview migration promise. Every future public schema must add an
|
||||
explicit migration fixture and rebuild campaign before its release can be
|
||||
called compatible.
|
||||
@@ -0,0 +1,148 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Retention and metric rollups
|
||||
|
||||
Observatory enforces the server policy in `server.json` after startup recovery
|
||||
and once per hour:
|
||||
|
||||
- hot log projections: 30 days;
|
||||
- hot trace projections: 30 days;
|
||||
- hot raw metric projections: 14 days;
|
||||
- immutable cold raw segments: preserved indefinitely by default;
|
||||
- five-minute metric rollups: 400 days; and
|
||||
- deployment events, resolved incidents, and security audit evidence: 400 days.
|
||||
|
||||
All values are explicit configuration. The list above is the shipped example,
|
||||
not a hidden fallback. `cold_raw_days` becomes a final raw-evidence cutoff only
|
||||
when `delete_cold_raw` is explicitly enabled. It is not an additional window
|
||||
after the hot cutoff.
|
||||
|
||||
## Hot and cold evidence
|
||||
|
||||
Raw batches are already independently Zstandard-compressed and addressed by
|
||||
their SHA-256 digest. Observatory does not wrap them in a gzip tarball: that
|
||||
would make selective verification and opening slower while usually saving
|
||||
little space over already-compressed input. When the newest observation in a
|
||||
segment leaves its hot window, Observatory atomically moves the exact object
|
||||
from `raw/` to `cold/` and advances its catalog tier. No telemetry is decoded,
|
||||
rewritten, or recompressed during that move.
|
||||
|
||||
Cold queries are intentionally allowed to take longer. Their explain estimate
|
||||
includes each selected segment's catalogued uncompressed size. Execution
|
||||
validates the catalog path and time range, verifies the compressed checksum,
|
||||
decodes a bounded segment, and applies the same tenant scope, sensitive-field
|
||||
authorization, typed filters, scan limit, memory limit, row limit, and timeout
|
||||
as a hot query. Matching cold metric segments force an exact raw query instead
|
||||
of mixing incomplete history with aggregate rollups.
|
||||
The explain plan adds `/cold:raw` whenever this tier participates.
|
||||
|
||||
The default therefore preserves forensic raw detail without an automatic
|
||||
destruction date. Projections and rollups still expire on schedule, so ordinary
|
||||
queries stay compact while a deliberate cold query can recover exact older
|
||||
evidence at an accepted additional cost. Full-disk encryption, protected
|
||||
backups, quota planning, and filesystem capacity remain operator
|
||||
responsibilities in the first preview.
|
||||
|
||||
Forensic retention applies only **after** Observatory's collection boundary.
|
||||
It does not mean collect everything: request and response bodies remain off by
|
||||
default, known credential and header deny rules run before spooling, source
|
||||
adapters use allowlists, and unknown fields remain sensitive and unindexed.
|
||||
An operator must still choose lawful inputs and retention appropriate to the
|
||||
people and systems represented by the evidence.
|
||||
|
||||
Observatory does not currently combine cold objects into `tar.gz` archives.
|
||||
Each raw batch is already zstd-compressed and content-addressed, so another
|
||||
compression layer would usually save little while making selective reads and
|
||||
independent checksum verification harder. A future high-inode-volume backend
|
||||
may pack many unchanged zstd objects into an immutable checksummed container
|
||||
with a separate bounded index. Such a pack must be completely written,
|
||||
verified, and catalogued before its source objects can be retired; packing must
|
||||
never change their logical digests or tenant boundaries.
|
||||
|
||||
## What a rollup retains
|
||||
|
||||
Every metric ingest updates its five-minute aggregate in the same SQLite
|
||||
transaction as the raw projection. Each aggregate retains exact count, sum,
|
||||
minimum, maximum, and the last sample value/timestamp plus a bounded deterministic
|
||||
histogram. `p50`, `p95`, and `p99` over rollups are approximations and the
|
||||
query result sets `statistics.approximate` to `true`. Count, sum, minimum,
|
||||
maximum, and average remain exact for the retained samples.
|
||||
|
||||
Longer retention never silently widens the data boundary. A dimension enters
|
||||
a rollup only when its descriptor is:
|
||||
|
||||
- reviewed or an Observatory built-in;
|
||||
- marked with the `metric` retention class;
|
||||
- public or internal, never sensitive; and
|
||||
- low or medium cardinality.
|
||||
|
||||
Unknown, sensitive, and high-cardinality attributes remain raw-only. The
|
||||
explain plan names `/rollup:5m` whenever the aggregate projection is selected.
|
||||
Queries that filter individual values, request sub-five-minute buckets, or
|
||||
need fields unavailable in the aggregate continue to use raw samples.
|
||||
|
||||
Metric values are bounded to an absolute value of `1e25`. This is far beyond
|
||||
ordinary duration, byte, counter, and host measurements while keeping sums
|
||||
and the fixed histogram domain finite under hostile input.
|
||||
|
||||
## Organization policy
|
||||
|
||||
An organization owner can shorten a policy:
|
||||
|
||||
```sh
|
||||
sudo observatory admin retention set \
|
||||
--config /etc/gamertan-observatory/server.json \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--raw-logs-days 14 \
|
||||
--raw-traces-days 14 \
|
||||
--raw-metrics-days 7 \
|
||||
--cold-raw-days 180 \
|
||||
--delete-cold-raw \
|
||||
--metric-rollups-days 180 \
|
||||
--evidence-days 180
|
||||
```
|
||||
|
||||
`cold_raw_days` cannot be shorter than any hot raw/evidence window. Omit
|
||||
`--delete-cold-raw` to preserve cold evidence indefinitely. Enabling deletion
|
||||
is an explicit shortening policy. Extending any server default—including
|
||||
turning off deletion when the server default enables it—additionally requires
|
||||
the exact organization ID and a positive byte quota larger than the
|
||||
organization's current stored data:
|
||||
|
||||
```sh
|
||||
--approve-extension ORGANIZATION_ID \
|
||||
--quota-bytes 10737418240
|
||||
```
|
||||
|
||||
The policy change and actor are recorded without telemetry values. A quota is
|
||||
checked before accepting another raw segment, using current raw/projection
|
||||
storage plus a conservative allowance for the new projection. Secret values
|
||||
never enter the policy, audit summary, command output, or process arguments.
|
||||
|
||||
## Deletion and recovery boundary
|
||||
|
||||
Projected observations expire at their individual hot timestamps. Raw
|
||||
segments are immutable, so a segment becomes cold only when its newest
|
||||
observation crosses that signal's hot cutoff. A batch spanning multiple
|
||||
timestamps can therefore keep its older members hot until the newest member
|
||||
expires; agents should keep batches time-local. The pending archive path is
|
||||
recorded first, the exact segment is verified and atomically moved, both
|
||||
directory entries are synchronized, and the tier is advanced. Startup recovery
|
||||
safely completes an interruption before or after the rename.
|
||||
|
||||
Only when `delete_cold_raw` is true does the final cold cutoff separately mark
|
||||
the segment retiring. Its
|
||||
remaining projection and per-segment rollup ledger rows are removed, its
|
||||
checksum/path are revalidated, its file and directory entry are synchronized,
|
||||
and finally its control record is deleted. A missing file is accepted only for
|
||||
the recorded interrupted-retirement state; changed bytes or another path stop
|
||||
recovery.
|
||||
|
||||
Metric rollups are disposable projections. Their expiration and SQLite
|
||||
compaction do not alter hot or cold raw evidence. Projection rebuilds use only
|
||||
hot segments so they do not turn the fast database back into an archive; the
|
||||
query engine opens cold evidence directly when requested. Once the final cold
|
||||
window expires under that explicit policy, rebuilding intentionally cannot
|
||||
recreate those deleted samples. Backups must follow the same published
|
||||
retention and deletion policy.
|
||||
@@ -0,0 +1,47 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Preview roadmap
|
||||
|
||||
`v0.1.0-preview.1` remains blocked until all of these work together:
|
||||
|
||||
- [x] Web Foundations organizations and scoped access are published and
|
||||
integrated for generated one-time bootstrap, forced password rotation,
|
||||
session revocation, and bounded query explanation.
|
||||
- [x] Complete tail-cursor recovery and enrollment around the implemented
|
||||
native collector adapters and durable 72-hour/5-GiB spool.
|
||||
- [x] Caddy, requestlog, and Tend event file adapters plus authenticated,
|
||||
bounded OTLP/HTTP protobuf ingestion for logs, metrics, and traces.
|
||||
- [x] Linux host, cgroup v2, filesystem, network, and explicitly selected
|
||||
process metrics in the durable unprivileged agent pipeline.
|
||||
- [x] Logs, metrics, traces, and deployment events projected and queryable.
|
||||
- [x] Add execution to the implemented unified builder/text AST, explain plan,
|
||||
sensitive-field permission, and hard planning budgets.
|
||||
- [x] Idempotent descriptive schema proposal persistence with organization-
|
||||
scoped review permission and no observed values in proposal metadata.
|
||||
- [x] Reviewed descriptor activation and atomic beside-current projection/index switching.
|
||||
- [x] Sandwich Hime dashboards, accessible alternatives, SSE, durable alert
|
||||
rules, an online incident inbox, installable shell, badge state, and an
|
||||
explicit opt-in read-only offline incident snapshot.
|
||||
- [x] Generic privacy-preserving Web Push with explicit browser opt-in,
|
||||
fixed encrypted content, reauthorization at delivery, and a bounded
|
||||
non-blocking queue.
|
||||
- [x] Strict Tend activation and rollback annotations whose identity, event-file,
|
||||
agent, network, or Observatory failures cannot control a release.
|
||||
- [x] Enforced hot/cold raw and evidence retention, approved organization
|
||||
overrides, serialized storage quotas, crash-recoverable archival and
|
||||
indefinite forensic preservation by default, explicitly enabled final
|
||||
retirement, budgeted forensic queries, projection compaction, and
|
||||
five-minute metric rollups with privacy-safe dimensions.
|
||||
- [ ] Security, corruption recovery, migration, and projection rebuild campaigns.
|
||||
- [ ] 4-vCPU/8-GiB capacity campaign and medium-fleet soak.
|
||||
- [ ] Sanitized public root snapshot, reproducible package, SBOM, and signed checksums.
|
||||
|
||||
## Post-preview direction
|
||||
|
||||
- [ ] Public-safe status dashboards backed by separately approved aggregate
|
||||
projections, coarse buckets, minimum group sizes, publication delay, and
|
||||
no unauthenticated path to internal or sensitive evidence.
|
||||
- [ ] An additive structured-frame source path for byte-faithful preservation
|
||||
of permitted producer records while retaining bounded framing,
|
||||
compression, authentication, tenant scope, and asynchronous parsing.
|
||||
Existing typed metrics and OTLP ingestion remain typed.
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Descriptive field schema
|
||||
|
||||
Every reviewed Observatory field descriptor records its signal, type, unit,
|
||||
meaning, sensitivity, cardinality budget, index policy, retention class, and
|
||||
projection version. Built-in fields such as normalized HTTP route and status
|
||||
already have explicit descriptors.
|
||||
|
||||
Unknown attribute keys remain queryable only with the sensitive-telemetry
|
||||
permission. They default to:
|
||||
|
||||
- sensitive;
|
||||
- high cardinality;
|
||||
- no index;
|
||||
- raw retention.
|
||||
|
||||
This includes client addresses, raw queries, referrers, user agents, and
|
||||
anonymous session identifiers selected through an agent source's explicit
|
||||
`sensitive_fields` policy. Collection and classification are separate review
|
||||
steps: opting a value into durable raw evidence does not make it public,
|
||||
low-cardinality, or indexed.
|
||||
|
||||
The reviewed sensitivity classes are deliberately small:
|
||||
|
||||
- `public`: eligible for an explicitly approved public-safe aggregate;
|
||||
- `internal`: ordinary authenticated organization telemetry; and
|
||||
- `sensitive`: separately authorized evidence such as client addresses,
|
||||
queries, referrers, user agents, session correlations, or unreviewed fields.
|
||||
|
||||
`public` is an eligibility label, not an automatic publication instruction.
|
||||
A future unauthenticated status view must compile a separate allowlisted
|
||||
aggregate projection with coarse buckets, minimum group sizes, a publication
|
||||
delay, and explicit organization approval. It must never expose the
|
||||
authenticated dashboard, raw records, identifiers, small groups, or query
|
||||
text. This public-safe projection is roadmap work rather than a capability of
|
||||
the current private preview.
|
||||
|
||||
`raw`, `metric`, and `evidence` are distinct lifecycle classes. `raw` survives
|
||||
the hot projection cutoff in the checksummed cold archive indefinitely by
|
||||
default. An organization can opt into an explicit final raw deletion window.
|
||||
Only reviewed,
|
||||
non-sensitive, bounded-cardinality metric dimensions marked `metric` enter the
|
||||
five-minute rollup projection. Selecting `metric` is therefore a retention
|
||||
decision as well as a query optimization; see
|
||||
[`RETENTION.md`](RETENTION.md).
|
||||
|
||||
After a raw segment is committed, Observatory records one aggregate proposal
|
||||
summary per unknown field in that segment. The segment digest, organization,
|
||||
and field form an idempotency key, so ingestion recovery cannot double count
|
||||
proposal evidence. The proposal contains occurrence count, estimated encoded
|
||||
bytes, an inferred type, first/last seen times, and a generic query using the
|
||||
field name. It never stores an observed value or telemetry body.
|
||||
The batch validator rejects more than 1,024 distinct attribute keys before raw
|
||||
segment commit so source-controlled field names cannot create unbounded schema
|
||||
work.
|
||||
|
||||
Organization owners can inspect pending proposals locally:
|
||||
|
||||
```sh
|
||||
observatory admin descriptors list \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID
|
||||
```
|
||||
|
||||
Operating the host or holding the platform-operator role does not grant this
|
||||
permission. Descriptor review uses the organization-scoped `schema.manage`
|
||||
grant.
|
||||
|
||||
## Review and activation
|
||||
|
||||
The list output supplies the inferred descriptor, occurrence count, estimated
|
||||
encoded bytes, first/last observation time, and generic example query. Review
|
||||
those values and create a complete descriptor JSON file. The file is an
|
||||
administrative input, so production requires an absolute, root-owned,
|
||||
mode-`0600`, regular non-symlink file; JSON is capped at 64 KiB and unknown
|
||||
properties fail closed.
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"signal": "metrics",
|
||||
"field": "workshop.queue_depth",
|
||||
"type": "integer",
|
||||
"unit": "items",
|
||||
"meaning": "Number of work items waiting in the selected service queue.",
|
||||
"sensitivity": "internal",
|
||||
"cardinality": "low",
|
||||
"index": "range",
|
||||
"retention": "raw",
|
||||
"projection_version": 1
|
||||
}
|
||||
```
|
||||
|
||||
The input projection version is a placeholder; Observatory assigns the next
|
||||
organization-local version during activation.
|
||||
|
||||
```sh
|
||||
observatory admin descriptors activate \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--descriptor-file /root/reviews/workshop.queue_depth.json
|
||||
```
|
||||
|
||||
Activation copies every currently active descriptor into a new immutable
|
||||
version, builds the complete typed custom-field index beside the current one,
|
||||
and changes the active version inside the same organization-database
|
||||
transaction. Invalid historical values stay in raw evidence but are not
|
||||
coerced into the index. New ingestion writes to the active version. Old index
|
||||
tables remain available for recovery, and retry repairs an interrupted
|
||||
control-database acknowledgement without rebuilding an already active,
|
||||
identical descriptor.
|
||||
|
||||
A proposal that should not become part of the reviewed schema can be rejected
|
||||
idempotently:
|
||||
|
||||
```sh
|
||||
observatory admin descriptors reject \
|
||||
--actor-user-id USER_ID \
|
||||
--organization-id ORGANIZATION_ID \
|
||||
--signal metrics \
|
||||
--field workshop.queue_depth
|
||||
```
|
||||
|
||||
An active descriptor cannot be rejected. Revision of an already active
|
||||
descriptor requires a future explicit review-proposal workflow; the current
|
||||
command will not silently reinterpret an active field.
|
||||
@@ -0,0 +1,147 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Security and recovery campaign
|
||||
|
||||
This document maps the current maintainer self-assessment to executable
|
||||
evidence. It is not an independent audit or a claim that the public preview is
|
||||
ready. The release commit, packaged binary, browser campaign, capacity run,
|
||||
and fleet soak must receive separate evidence before the preview tag.
|
||||
|
||||
## Current executable evidence
|
||||
|
||||
| Boundary | Executable evidence |
|
||||
| --- | --- |
|
||||
| Organization isolation and scoped queries | `TestBootstrapSeparatesPlatformAndOrganizationAccess`, `TestTeamsInvitationsRevocationAndBreakGlassRemainOrganizationScoped`, `TestPlanRejectsCrossTenantScopeAndScanBudget`, `TestCredentialScopeCannotBeOverridden` |
|
||||
| Invitations, teams, revocation, and break-glass audit/expiry | `TestTeamsInvitationsRevocationAndBreakGlassRemainOrganizationScoped` plus the pinned Web Foundations package tests |
|
||||
| Generated bootstrap credential, forced first-login rotation, and local recovery | `TestAdminBootstrapGeneratesExclusiveOneTimeCredential`, `TestAdminBootstrapSupportsConfinedSystemdCredentials`, `TestAdminHierarchyAndEnrollmentSupportConfinedSystemdCredentials`, `TestAdminUserResetPasswordGeneratesPrivateCredentialAndRevokesSessions`, `TestTemporaryOperatorMustRotatePasswordBeforeUsingApplication`, `TestAPITemporaryOperatorReceivesScopedRotationToken`, plus pinned Web Foundations password-change and atomic recovery tests |
|
||||
| Token-bound HTML forms with privacy-browser compatibility and cross-site rejection | `TestHTMLLoginUsesTokenWhenBrowserOmitsOrObscuresOriginMetadata`, `TestHTMLLoginFailsClosed`, `TestTemporaryOperatorMustRotatePasswordBeforeUsingApplication`, `TestDashboardManagementIsScopedCSRFProtectedAndExportable`, `TestIncidentRulesEvaluationInboxAndResponseAreScoped` |
|
||||
| Replay, exact logical-batch acknowledgement, duplicate batches, sequence gaps, enrollment, and source revocation | `TestScopedIngestionDeduplicationAndReplay`, `TestNativeEnvelopeReplayUsesBatchIdentityAndAllowsOverlappingTime`, `TestFramedNativeIngestAcknowledgesExactReplayAndOverlappingTime`, `TestSendAcceptsRealServerBatchDigestRatherThanPrivateSegmentDigest`, `TestRunnerPreservesSpoolOnMismatchedAcknowledgement`, `TestEnrollmentIsScopedExpiringAndSingleUse`, `TestAgentEnrollmentIsSingleUseAndCredentialCanSelfRevoke` |
|
||||
| Native envelope and compressed or malformed OTLP framing | `TestEnvelopeHeadersRoundTripAndRejectAmbiguity`, `FuzzParseEnvelopeHeaders`, `TestOTLPHTTPIngestionIsAuthenticatedBoundedAndCompressed`, `TestDecodeRejectsMalformedAndNonFiniteData`, `FuzzDecode` |
|
||||
| Cardinality, timestamp, field, query, scan, memory, and result bounds | `TestBatchRejectsDistinctFieldCardinalityAbuse`, `TestBatchClockSkewAndRetentionWindowsFailClosed`, `TestQueryEnforcesSensitiveAndExecutionBudgets`, `FuzzParse` |
|
||||
| Secret minimization and safe error output | `TestCaddyCollectorDropsSecretsAndQuery`, `TestRequestLogCollectorUsesWhitelist`, `TestDecodeLogsDropsCredentialsAndPreservesTelemetry`, `TestIngestDoesNotExposeValuesInErrors` |
|
||||
| Filesystem and interrupted-write boundaries | `TestStorageRejectsSymlinkedSQLiteFilesAndProjectionDirectories`, `TestStateAtomicRoundTripAndSymlinkRefusal`, `TestSpoolRejectsQuotaAndSymlink`, `TestTailerPreservesPartialLineAndRecoversRotation`, `TestInterruptedTemporarySegmentIsIgnoredUntilAtomicCommit` |
|
||||
| Raw corruption and crash recovery | `TestCommitReadAndCorruption`, `TestRecoveryIndexesRawSegmentMissingFromControlDatabase`, `TestProjectionRebuildFailurePreservesLiveProjection` |
|
||||
| Complete projection reconstruction | `TestProjectionRebuildRestoresRawTruthAndActivatedDescriptorsAtomically`, `TestProjectionRebuildRejectsUnknownOrganization`, `TestProjectionRebuildRefusesSymlinkProjectionOrSidecar` |
|
||||
| Migration locking and schema fixtures | `TestProcessLockSeparatesLiveServerFromOfflineMigration`, `TestOfflineMigrationRefusesLiveDataDirectory`, `TestControlSchemaFourMigratesToIncidentSchema`, `TestControlSchemaFiveMigratesToPushSchema` |
|
||||
| PWA, SSE, incident scope, and generic push content | `TestManifestAndServiceWorkerUseExactContentAddressedShell`, `TestLiveRefreshStreamIsAuthorizedAndCarriesNoTelemetry`, `TestIncidentRulesEvaluationInboxAndResponseAreScoped`, `TestSenderUsesEncryptedGenericPayloadAndValidVAPID` |
|
||||
| Tend input and non-authoritative deployment evidence | `TestTendCollectorIsStrict`, `FuzzTendCollector`; producer activation/rollback failure injection remains authoritative in Tend's own repository |
|
||||
|
||||
The migration fixtures cover every internal control schema that predates this
|
||||
candidate and can reach its current schema. Observatory has not published a
|
||||
preview, so no public-version migration claim exists yet. Each future public
|
||||
preview must retain a fixture and a raw-projection rebuild path.
|
||||
|
||||
## Reproduction
|
||||
|
||||
The ordinary verifier pins Go and Hime-san, checks generated-source freshness
|
||||
and no-op determinism, then runs tests, the race detector, vet, a trimmed
|
||||
build, and the production dependency boundary:
|
||||
|
||||
```sh
|
||||
GOCACHE=/tmp/observatory-go-cache ./scripts/verify.sh
|
||||
```
|
||||
|
||||
The bounded adversarial campaign adds query, OTLP, and Tend-event fuzzing and
|
||||
reruns the security-critical packages under the race detector:
|
||||
|
||||
```sh
|
||||
OBSERVATORY_FUZZ_TIME=30s \
|
||||
GOCACHE=/tmp/observatory-go-cache \
|
||||
./scripts/security-campaign.sh
|
||||
```
|
||||
|
||||
On August 17, 2026, a 10-second-per-target development run completed without
|
||||
an invariant failure: approximately 2.45 million query-parser cases, 670,000
|
||||
OTLP cases, and 166,000 strict Tend-event cases, followed by the uncached race
|
||||
matrix. Counts are observations from one run, not minimum performance claims.
|
||||
|
||||
Trusted Gitea assurance run 232 then exercised source commit
|
||||
`14125387d13947eb4a523e95c2e441ff89abcc1a` (tree
|
||||
`f378872450054e7ff6f5499c61a5db0e4a3d3da7`) with the pinned Go 1.26.6
|
||||
toolchain and `govulncheck` v1.6.0. It completed 1,092,559 query-parser,
|
||||
398,595 OTLP-decoder, and 324,020 Tend-event fuzz executions in three separate
|
||||
30-second targets, then passed the uncached security-package race matrix and
|
||||
the ordinary deterministic verifier. The checkout remained unchanged.
|
||||
|
||||
The same scan reported zero reachable vulnerabilities and zero
|
||||
vulnerabilities in imported packages. It also reported GO-2026-5932 in the
|
||||
required `golang.org/x/crypto` module because its legacy `openpgp` package is
|
||||
unmaintained. Observatory and its Web Foundations dependency do not import
|
||||
that package, and the advisory has no fixed module version. This is a recorded
|
||||
dependency boundary, not a claim that the module-only advisory was repaired.
|
||||
|
||||
The resource-limited ingestion, query, outage-replay, and retention gate is
|
||||
defined separately in [`CAPACITY.md`](CAPACITY.md). Its short development mode
|
||||
is suitable for implementation feedback; only the exact one-hour release mode
|
||||
can close the capacity item below.
|
||||
|
||||
## Open release evidence
|
||||
|
||||
- Exercise one granted browser-vendor Web Push delivery and OS notification
|
||||
activation in a headed supported browser. The networkless Chromium campaign
|
||||
below proves the application and worker boundaries without contacting a push
|
||||
relay or depending on a desktop notification service.
|
||||
- Complete the specified 4-vCPU/8-GiB capacity campaign, outage replay,
|
||||
retention, compaction, and concurrent-organization workloads. The
|
||||
August 17 constrained run passed its one-hour 2,000-observation/second
|
||||
sustain boundary but missed the 10,000-observation/second burst boundary;
|
||||
[`CAPACITY.md`](CAPACITY.md) records the measured result and remaining work.
|
||||
- Complete the medium-fleet dogfood soak. Observatory's first real Tend
|
||||
activation, rollback, and identical-artifact redeployment passed on
|
||||
August 17; [`DEPLOYMENT.md`](DEPLOYMENT.md) records that bounded result.
|
||||
- Re-run this campaign against the exact packaged release commit and record
|
||||
its immutable artifact digest, SBOM, checksum, and signature.
|
||||
|
||||
## Real-browser campaign
|
||||
|
||||
`scripts/browser-campaign.sh` builds a build-tagged, disposable HTTPS fixture
|
||||
and drives Chromium through pinned Playwright 1.62.1. The approved Linux image
|
||||
is `mcr.microsoft.com/playwright@sha256:dcc5531e97840b9b5e794f2814476b21571c5124a3fca2267d73041f56e7580e`.
|
||||
The fixture and browser use loopback only. The campaign rejects every HTTP
|
||||
request to another origin and verifies manifest installability, the public
|
||||
offline shell, explicit private-inbox caching and sign-out deletion, SSE
|
||||
reconnection, app badging, generic notification content and activation,
|
||||
keyboard entry, landmarks, forced colors, reduced motion, and 320-pixel
|
||||
overflow.
|
||||
|
||||
On August 17, 2026, the campaign completed twice in pinned headless Chromium
|
||||
inside a disposable, networkless Linux container limited to four CPUs and
|
||||
8 GiB. Both runs reported every evidence field true and zero external HTTP
|
||||
requests. They exercised a real service worker, cache storage, EventSource,
|
||||
session, incident inbox, application badge, and the production push and
|
||||
notification-click handlers. Headless Chromium denied the intentional
|
||||
user-triggered notification permission request, so the worker's fixed-shape
|
||||
push event and activation path were invoked directly; the prohibited OS focus
|
||||
side effect was replaced with an in-worker client test double. This proves the
|
||||
payload and activation contract, not end-to-end browser-vendor delivery. The
|
||||
exact npm lockfile audit reported zero known vulnerabilities at all severities.
|
||||
|
||||
The disposable fixture uses a one-hour self-signed `localhost` certificate and
|
||||
passes only its ephemeral SHA-256 subject-public-key fingerprint to Chromium's
|
||||
SPKI allowlist. Chromium serializes `Origin: null` on form submissions under
|
||||
that synthetic certificate while Fetch Metadata still reports `same-origin`.
|
||||
The fixture does not rewrite those headers: the campaign exercises the same
|
||||
token-bound HTML form policy used in production and asserts the observed
|
||||
metadata. The token remains mandatory, and explicit cross-site metadata still
|
||||
fails closed. This is not a general TLS-origin acceptance test.
|
||||
|
||||
To test native EventSource recovery deterministically, the build-tagged
|
||||
fixture cancels the real authenticated `/app/events` request after 1.25
|
||||
seconds. The browser must reconnect and receive a later generic refresh event.
|
||||
The production handler, authorization, event format, and connection lifetime
|
||||
are not changed.
|
||||
|
||||
Install the exact development dependency without downloading a browser when a
|
||||
pinned Playwright container supplies Chromium:
|
||||
|
||||
```sh
|
||||
cd test/browser
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci --ignore-scripts
|
||||
cd ../..
|
||||
./scripts/browser-campaign.sh
|
||||
```
|
||||
|
||||
The resulting JSON contains pass/fail booleans and no machine, user, token,
|
||||
organization, incident, path, or telemetry identifiers. A development pass is
|
||||
not release evidence until repeated against the packaged release commit and
|
||||
the pinned container digest is recorded.
|
||||
@@ -0,0 +1,76 @@
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
|
||||
# Tend deployment evidence
|
||||
|
||||
Observatory consumes Tend's bounded version-1 deployment-event JSONL as an
|
||||
unprivileged, read-only file source. Tend does not call Observatory and never
|
||||
waits for an Observatory acknowledgement. Identity, entropy, event-file, agent,
|
||||
network, and server failures therefore cannot decide whether Tend activates or
|
||||
rolls back an application.
|
||||
|
||||
Each accepted record has exactly these fields:
|
||||
|
||||
- a cryptographically random operation ID;
|
||||
- service, approved artifact SHA-256, source commit, and release version;
|
||||
- phase, optional slot, elapsed milliseconds, outcome, and UTC observation
|
||||
time.
|
||||
|
||||
The adapter rejects unknown fields, trailing JSON, negative durations,
|
||||
malformed identifiers, unsafe values, invalid timestamps, and records larger
|
||||
than Tend's 4-KiB producer limit. It discards the source line after converting
|
||||
only this fixed field set into a deployment observation. No environment value,
|
||||
secret path, HTTP body, command output, or arbitrary process output belongs to
|
||||
the contract.
|
||||
|
||||
## Agent configuration
|
||||
|
||||
Grant the dedicated Observatory agent account read access only to the selected
|
||||
service's event file, then configure an explicit local source:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "tend_events_jsonl",
|
||||
"path": "/opt/example/deployment-events.jsonl",
|
||||
"stream_id": "tend-deployments"
|
||||
}
|
||||
```
|
||||
|
||||
The source's organization, project, environment, and service scope comes from
|
||||
its server-side enrollment. Neither the file nor a deployment event may choose
|
||||
tenant scope. The normal durable cursor and spool rules apply during rotation,
|
||||
agent restarts, or an Observatory outage.
|
||||
|
||||
## Querying
|
||||
|
||||
Activation and rollback attempts share the `tend.deployment` observation name
|
||||
and correlate through `deployment.operation_id`:
|
||||
|
||||
```text
|
||||
deployments
|
||||
| where service.name == "example-site"
|
||||
| sort deployment.duration_ms desc
|
||||
| limit 50
|
||||
```
|
||||
|
||||
Keep candidate, activation, and rollback phases together when reconstructing a
|
||||
release operation. A missing event means evidence was unavailable; it must not
|
||||
be interpreted as proof that a deployment did not occur. Tend's own state and
|
||||
the application's active release remain authoritative for deployment control.
|
||||
|
||||
The producer contract is maintained by [Gamertan Tend](https://gitea.speelman.ca/gamertan/tend).
|
||||
|
||||
## Production dogfood observation
|
||||
|
||||
On August 18, 2026, one checksum-approved Tend candidate exercised candidate
|
||||
validation, activation, explicit rollback, and reactivation for Gamertan,
|
||||
Sandwich Hime, and Observatory. The production agent then projected all three
|
||||
bounded event files through separate streams: ten Gamertan events and six for
|
||||
each of the two singleton services. Gamertan's stream intentionally includes
|
||||
two failed pre-activation candidates that never received public traffic; those
|
||||
records helped identify a hardened-umask extraction defect before the corrected
|
||||
candidate completed all three service cycles.
|
||||
|
||||
This observation demonstrates the producer-to-agent-to-projection path and its
|
||||
failure evidence. It is not a substitute for Tend state, active-release
|
||||
inspection, public health checks, or the remaining Observatory fleet and
|
||||
capacity release gates.
|
||||
Reference in New Issue
Block a user