@alisio/plugin-telemetry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/telemetry/SKILL.md +52 -0
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/cover.svg +54 -0
- package/dist/commands.d.ts +19 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +298 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +145 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +524 -0
- package/dist/config.js.map +1 -0
- package/dist/database.d.ts +16 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +67 -0
- package/dist/database.js.map +1 -0
- package/dist/events.d.ts +135 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +255 -0
- package/dist/events.js.map +1 -0
- package/dist/format.d.ts +36 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +49 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/otlp.d.ts +69 -0
- package/dist/otlp.d.ts.map +1 -0
- package/dist/otlp.js +616 -0
- package/dist/otlp.js.map +1 -0
- package/dist/redact.d.ts +36 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +163 -0
- package/dist/redact.js.map +1 -0
- package/dist/runtime.d.ts +80 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +290 -0
- package/dist/runtime.js.map +1 -0
- package/dist/store.d.ts +188 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +539 -0
- package/dist/store.js.map +1 -0
- package/dist/tools.d.ts +21 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +292 -0
- package/dist/tools.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telemetry
|
|
3
|
+
description: "Trigger: latency, token cost, tool failures, regressions, usage questions. Read bounded telemetry summaries from the local store before guessing."
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: "alisio-contributors"
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Activation Contract
|
|
11
|
+
|
|
12
|
+
Load this skill when a question is about how the agent has been performing, costing, or failing:
|
|
13
|
+
slow turns, token usage, model mix, tool error rate, recent sessions, or a suspected regression.
|
|
14
|
+
Telemetry is local-first and privacy-preserving; consult the local store through the read-only
|
|
15
|
+
`telemetry_*` tools instead of guessing.
|
|
16
|
+
|
|
17
|
+
## Hard Rules
|
|
18
|
+
|
|
19
|
+
- Telemetry is evidence about past runs, not a substitute for reading the current code. Verify a
|
|
20
|
+
regression against the actual change before blaming it.
|
|
21
|
+
- Every tool returns a bounds envelope (`returned`, `total`, `truncated`, `hint`). When `truncated`
|
|
22
|
+
is true, narrow the time window or raise `limit`; never assume the visible slice is the whole story.
|
|
23
|
+
- Content search only returns text when content capture is explicitly enabled. When capture is off,
|
|
24
|
+
answer from metadata (counts, tokens, latency) and say that content is not recorded.
|
|
25
|
+
- Telemetry can reveal how someone works, which repositories they use, and what they type. Treat any
|
|
26
|
+
captured content as sensitive: do not paste it elsewhere, and never enable capture without the
|
|
27
|
+
operator's explicit decision.
|
|
28
|
+
- Remote export is opt-in. Local-only is the default; do not claim data left the machine unless
|
|
29
|
+
remote export is configured and a flush succeeded.
|
|
30
|
+
- Never ask for or handle the OTLP credential. It exists only as an environment variable read at
|
|
31
|
+
export time.
|
|
32
|
+
|
|
33
|
+
## Decision Gates
|
|
34
|
+
|
|
35
|
+
| Question | Tool | Notes |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Overall volume, errors, latency | `telemetry_summary` | Start here; default window is one day |
|
|
38
|
+
| Which models, and their token cost | `telemetry_models` | Compare input/output and cached input |
|
|
39
|
+
| Which tools fail or get slow | `telemetry_tools` | Look at error rate and effects |
|
|
40
|
+
| What happened recently | `telemetry_sessions` | Bounded session/run metadata |
|
|
41
|
+
| Find a specific captured text | `telemetry_search` | Requires content capture |
|
|
42
|
+
|
|
43
|
+
## Execution Steps
|
|
44
|
+
|
|
45
|
+
1. Pick a bounded window (`windowMinutes`) and a small `limit`; start with `telemetry_summary`.
|
|
46
|
+
2. Drill into `telemetry_models` and `telemetry_tools` to locate the regression.
|
|
47
|
+
3. Correlate timing with a code change, then confirm by reading the code.
|
|
48
|
+
4. If a store is empty or unavailable, say so plainly and continue without telemetry.
|
|
49
|
+
|
|
50
|
+
## References
|
|
51
|
+
|
|
52
|
+
- `README.md` in this package documents configuration, privacy defaults, and deletion.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gustavo Gutiérrez Mercado
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @alisio/plugin-telemetry
|
|
2
|
+
|
|
3
|
+
Privacy-first local agent observability for
|
|
4
|
+
[Alisio](https://github.com/GustavoGutierrez/alisio). It records what the agent
|
|
5
|
+
does into a local SQLite database, exposes bounded read-only query tools, and can
|
|
6
|
+
optionally export OpenTelemetry-aligned data over OTLP. Local-only is the default;
|
|
7
|
+
remote export is opt-in.
|
|
8
|
+
|
|
9
|
+
This is an independent plugin. It is not affiliated with the OpenTelemetry
|
|
10
|
+
project and claims no conformance to a stable semantic-convention release.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
alisio install npm:@alisio/plugin-telemetry
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
- Observes Alisio `RunEvent`s through a synchronous, non-blocking handler and
|
|
21
|
+
writes compact records to a local SQLite database.
|
|
22
|
+
- Serves five read-only tools that return bounded, aggregated, privacy-preserving
|
|
23
|
+
summaries.
|
|
24
|
+
- Optionally exports traces, logs and metrics to an OTLP/HTTP endpoint with
|
|
25
|
+
durable, idempotent batching.
|
|
26
|
+
- Ships a `telemetry` agent skill explaining when and how to consult it.
|
|
27
|
+
|
|
28
|
+
## Privacy defaults
|
|
29
|
+
|
|
30
|
+
- **Metadata-only by default.** Prompt, completion, tool-argument and tool-result
|
|
31
|
+
content capture is OFF unless explicitly enabled.
|
|
32
|
+
- **Local-only by default.** No network call is made until remote export is
|
|
33
|
+
enabled with an endpoint.
|
|
34
|
+
- When content capture is enabled, every captured value passes through
|
|
35
|
+
allowlist-based redaction: bearer/authorization headers, tokens, private keys,
|
|
36
|
+
secrets in key/value form, absolute machine paths and (in `strict` mode) long
|
|
37
|
+
high-entropy runs are stripped, and every field is bounded. Redaction is never
|
|
38
|
+
denylist-only.
|
|
39
|
+
- **The OTLP credential is never persisted, logged, returned or serialized.** It
|
|
40
|
+
is read at export time only, from the environment variable named by
|
|
41
|
+
`otlp.tokenEnv` (default `ALISIO_TELEMETRY_OTLP_TOKEN`). The config file may
|
|
42
|
+
only reference the variable's name.
|
|
43
|
+
- Telemetry can reveal how someone works, which repositories they use, and what
|
|
44
|
+
they type. Keep `strict` redaction on, prefer metadata-only, and set a
|
|
45
|
+
retention window you are comfortable with.
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Configuration precedence, highest first:
|
|
50
|
+
|
|
51
|
+
1. Explicit non-secret command/tool argument (for example
|
|
52
|
+
`/telemetry:telemetry-setup --retention-days 7`).
|
|
53
|
+
2. Environment variables.
|
|
54
|
+
3. The config file at `<configHome>/telemetry/config.json`.
|
|
55
|
+
4. Built-in safe defaults.
|
|
56
|
+
|
|
57
|
+
`configHome` resolves as `ALISIO_CONFIG_HOME`, else `XDG_CONFIG_HOME/alisio`,
|
|
58
|
+
else `~/.config/alisio`. `stateHome` resolves as `ALISIO_STATE_HOME`, else
|
|
59
|
+
`XDG_STATE_HOME/alisio`, else `~/.local/state/alisio`.
|
|
60
|
+
|
|
61
|
+
The database path is `ALISIO_TELEMETRY_DB`, else
|
|
62
|
+
`<stateHome>/telemetry/telemetry.sqlite`. Telemetry stores its data through the
|
|
63
|
+
host-provided SQLite storage port (`api.storage.sqlite`), so the plugin never
|
|
64
|
+
depends on a runtime SQLite driver. Because the host port sets no pragmas, the
|
|
65
|
+
plugin hardens the port right after opening: it enables WAL, a `busy_timeout`,
|
|
66
|
+
foreign keys and `NORMAL` synchronous mode, ensures parent directories are
|
|
67
|
+
`0700` and the database file is `0600`, and periodically runs `PRAGMA optimize`.
|
|
68
|
+
|
|
69
|
+
An invalid configuration **fails closed**: telemetry is disabled and an actionable
|
|
70
|
+
message is reported. It never crashes the host.
|
|
71
|
+
|
|
72
|
+
### Settings
|
|
73
|
+
|
|
74
|
+
| Field | Meaning |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `retentionDays` | How long records are kept before `telemetry-prune` removes them |
|
|
77
|
+
| `capture.prompts` / `.completions` / `.toolArguments` / `.toolResults` | Opt-in content capture (redacted, bounded) |
|
|
78
|
+
| `redaction.mode` | `strict` (default) or `standard` |
|
|
79
|
+
| `otlp.enabled` | Master switch for remote export |
|
|
80
|
+
| `otlp.endpoint` | OTLP/HTTP base URL, e.g. `https://collector.example.com:4318` |
|
|
81
|
+
| `otlp.headers` | Non-secret headers only; secret-shaped headers are refused |
|
|
82
|
+
| `otlp.tokenEnv` | Name of the environment variable that holds the token |
|
|
83
|
+
| `otlp.serviceName` / `.environment` / `.instanceId` | Resource attributes |
|
|
84
|
+
| `otlp.samplingRatio` | Deterministic per-run sampling, `0`–`1` |
|
|
85
|
+
| `otlp.signals.traces` / `.logs` / `.metrics` | Which signals to export |
|
|
86
|
+
| `otlp.gzip` | Gzip the request body |
|
|
87
|
+
| `otlp.maxAttempts` / `.timeoutMs` / `.batchSize` | Export bounds |
|
|
88
|
+
| `batch.batchSize` / `.flushIntervalMs` / `.maxQueue` | Local write bounds |
|
|
89
|
+
|
|
90
|
+
### Environment variables
|
|
91
|
+
|
|
92
|
+
`ALISIO_TELEMETRY_DB`, `ALISIO_CONFIG_HOME`, `ALISIO_STATE_HOME`,
|
|
93
|
+
`XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `ALISIO_TELEMETRY_RETENTION_DAYS`,
|
|
94
|
+
`ALISIO_TELEMETRY_OTLP_ENABLED`, `ALISIO_TELEMETRY_OTLP_ENDPOINT`,
|
|
95
|
+
`ALISIO_TELEMETRY_OTLP_TOKEN_ENV`, `ALISIO_TELEMETRY_OTLP_INSTANCE_ID`,
|
|
96
|
+
`ALISIO_TELEMETRY_SERVICE_NAME`, `ALISIO_TELEMETRY_ENVIRONMENT`,
|
|
97
|
+
`ALISIO_TELEMETRY_SAMPLING_RATIO`, `ALISIO_TELEMETRY_CAPTURE_PROMPTS`,
|
|
98
|
+
`ALISIO_TELEMETRY_CAPTURE_COMPLETIONS`, `ALISIO_TELEMETRY_CAPTURE_TOOL_ARGUMENTS`,
|
|
99
|
+
`ALISIO_TELEMETRY_CAPTURE_TOOL_RESULTS`, `ALISIO_TELEMETRY_REDACTION_MODE`,
|
|
100
|
+
`ALISIO_TELEMETRY_OTLP_SIGNALS`, `ALISIO_TELEMETRY_OTLP_GZIP`,
|
|
101
|
+
`ALISIO_TELEMETRY_BATCH_SIZE`, `ALISIO_TELEMETRY_FLUSH_INTERVAL_MS`,
|
|
102
|
+
`ALISIO_TELEMETRY_MAX_QUEUE`.
|
|
103
|
+
|
|
104
|
+
The token itself is read only from `ALISIO_TELEMETRY_OTLP_TOKEN` (or the variable
|
|
105
|
+
named by `otlp.tokenEnv`).
|
|
106
|
+
|
|
107
|
+
## Tools
|
|
108
|
+
|
|
109
|
+
All tools are read-only (`read`), closed-schema, and return a bounds envelope
|
|
110
|
+
(`window`, `returned`, `total`, `truncated`, `hint`). They never dump raw events
|
|
111
|
+
and never expose secrets or absolute machine paths.
|
|
112
|
+
|
|
113
|
+
| Tool | Returns |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `telemetry_summary` | Sessions, runs, turns, tokens, tool errors, latency and a time series |
|
|
116
|
+
| `telemetry_models` | Model mix with token usage, calls and average turn duration |
|
|
117
|
+
| `telemetry_tools` | Tool call counts, error rate, latency and effects |
|
|
118
|
+
| `telemetry_sessions` | Recent sessions/runs with bounded metadata |
|
|
119
|
+
| `telemetry_search` | Bounded content search; only returns content when capture is enabled |
|
|
120
|
+
|
|
121
|
+
## Commands
|
|
122
|
+
|
|
123
|
+
Alisio namespaces external plugin commands as `<plugin id>:<name>`, so the real
|
|
124
|
+
invocation form is:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
/telemetry:telemetry-setup [options]
|
|
128
|
+
/telemetry:telemetry-status
|
|
129
|
+
/telemetry:telemetry-flush
|
|
130
|
+
/telemetry:telemetry-prune [--days <n>]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`telemetry-setup` persists configuration non-interactively and reports exactly
|
|
134
|
+
which fields changed, without ever echoing a credential. Use `--json` on any of
|
|
135
|
+
these for machine-readable output.
|
|
136
|
+
|
|
137
|
+
## OpenTelemetry alignment
|
|
138
|
+
|
|
139
|
+
The plugin emits OTLP/HTTP JSON to `/v1/traces`, `/v1/logs` and `/v1/metrics`
|
|
140
|
+
under the configured base endpoint. It uses standard names where they exist, for
|
|
141
|
+
example `gen_ai.operation.name`, `gen_ai.provider.name`, `gen_ai.request.model`,
|
|
142
|
+
`gen_ai.response.model`, `gen_ai.usage.input_tokens`,
|
|
143
|
+
`gen_ai.usage.output_tokens`, `gen_ai.usage.cached_input_tokens`,
|
|
144
|
+
`gen_ai.tool.name`, `gen_ai.tool.call.id`, `gen_ai.conversation.id`, and the
|
|
145
|
+
stable core attributes `error.type`, `server.address` and `server.port`. Span
|
|
146
|
+
names follow `{gen_ai.operation.name} {target}`, using `invoke_agent`, `chat` and
|
|
147
|
+
`execute_tool`.
|
|
148
|
+
|
|
149
|
+
**Honesty constraint.** Every `gen_ai.*` semantic convention is currently
|
|
150
|
+
Development/experimental, not stable, and the GenAI conventions moved out of the
|
|
151
|
+
main semantic-conventions repository. There is no stable release for metrics.
|
|
152
|
+
These attribute names may change and this plugin claims no stability. Where no
|
|
153
|
+
standard exists — cost, retry rate, cache-hit ratio, approval outcomes, context
|
|
154
|
+
growth — the plugin uses clearly namespaced `alisio.telemetry.*` attributes or
|
|
155
|
+
metrics instead of inventing `gen_ai.*` names. Cost and approval outcomes are not
|
|
156
|
+
captured at all.
|
|
157
|
+
|
|
158
|
+
Export behavior: HTTP 200 (including a partial-success body) is success and is
|
|
159
|
+
never retried; only `429`, `502`, `503` and `504` are retried, with bounded
|
|
160
|
+
attempts and floor-ed exponential backoff honoring `Retry-After`. Redirects are
|
|
161
|
+
refused and response bodies are read through a hard cap. Payloads are persisted as
|
|
162
|
+
batches keyed by their content hash before any network call, so a failed export
|
|
163
|
+
stays queued and a retry is idempotent — data is neither lost nor duplicated
|
|
164
|
+
silently. Non-retryable responses are marked `dead` and surfaced in
|
|
165
|
+
`telemetry-status` rather than discarded.
|
|
166
|
+
|
|
167
|
+
## Deleting telemetry
|
|
168
|
+
|
|
169
|
+
`/telemetry:telemetry-prune [--days <n>]` deletes runs, turns, tool calls and
|
|
170
|
+
content older than the cutoff, purges the full-text search index, then runs
|
|
171
|
+
`optimize` and `VACUUM`. To remove everything, run it with a cutoff in the
|
|
172
|
+
future (for example `--days 0`), or delete the SQLite file directly.
|
|
173
|
+
|
|
174
|
+
**FTS caveat:** the full-text search shadow tables (`content_fts\*`) can retain
|
|
175
|
+
fragments even after rows are deleted; prune clears the index and vacuums, but a
|
|
176
|
+
copy of the database made earlier may still contain forensic traces. Treat any
|
|
177
|
+
captured content as sensitive.
|
|
178
|
+
|
|
179
|
+
## Requirements
|
|
180
|
+
|
|
181
|
+
Node.js **>= 22.16** and an Alisio host with `@alisio/sdk`
|
|
182
|
+
`>=0.1.0-alpha.10` that provides the SQLite storage port
|
|
183
|
+
(`api.storage.sqlite`). Zero runtime dependencies: Node built-ins and native
|
|
184
|
+
`fetch` only.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT.
|
package/cover.svg
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Telemetry</title>
|
|
3
|
+
<desc id="desc">A dashboard with a token-usage chart, tool-call bars, and a local database cylinder feeding an opt-in export arrow.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
6
|
+
<stop offset="0" stop-color="#07111f"/>
|
|
7
|
+
<stop offset="1" stop-color="#0d1c31"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
|
|
10
|
+
<stop offset="0" stop-color="#22d3ee"/>
|
|
11
|
+
<stop offset="1" stop-color="#6366f1"/>
|
|
12
|
+
</linearGradient>
|
|
13
|
+
<linearGradient id="area" x1="0" y1="0" x2="0" y2="1">
|
|
14
|
+
<stop offset="0" stop-color="#22d3ee" stop-opacity="0.55"/>
|
|
15
|
+
<stop offset="1" stop-color="#22d3ee" stop-opacity="0.02"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
</defs>
|
|
18
|
+
<rect width="1600" height="900" fill="url(#bg)"/>
|
|
19
|
+
<rect x="120" y="110" width="940" height="520" rx="32" fill="#0b1a2e" stroke="#1e3a5f" stroke-width="6"/>
|
|
20
|
+
<rect x="170" y="160" width="250" height="22" rx="11" fill="#94a3b8" opacity="0.55"/>
|
|
21
|
+
<rect x="170" y="196" width="150" height="22" rx="11" fill="#38bdf8" opacity="0.75"/>
|
|
22
|
+
<path d="M180 540 L320 470 L460 500 L600 370 L740 410 L880 300 L1020 340" fill="none" stroke="url(#accent)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
|
23
|
+
<path d="M180 540 L320 470 L460 500 L600 370 L740 410 L880 300 L1020 340 L1020 560 L180 560 Z" fill="url(#area)"/>
|
|
24
|
+
<g fill="#1b2f4d" stroke="#2b4a76" stroke-width="4">
|
|
25
|
+
<rect x="170" y="250" width="70" height="290" rx="10"/>
|
|
26
|
+
<rect x="270" y="320" width="70" height="220" rx="10"/>
|
|
27
|
+
<rect x="370" y="290" width="70" height="250" rx="10"/>
|
|
28
|
+
<rect x="470" y="380" width="70" height="160" rx="10"/>
|
|
29
|
+
<rect x="570" y="300" width="70" height="240" rx="10"/>
|
|
30
|
+
</g>
|
|
31
|
+
<g fill="#38bdf8" opacity="0.85">
|
|
32
|
+
<rect x="170" y="470" width="70" height="70" rx="10"/>
|
|
33
|
+
<rect x="270" y="470" width="70" height="70" rx="10"/>
|
|
34
|
+
<rect x="370" y="470" width="70" height="70" rx="10"/>
|
|
35
|
+
<rect x="470" y="470" width="70" height="70" rx="10"/>
|
|
36
|
+
<rect x="570" y="470" width="70" height="70" rx="10"/>
|
|
37
|
+
</g>
|
|
38
|
+
<g transform="translate(1140 150)">
|
|
39
|
+
<ellipse cx="160" cy="70" rx="150" ry="46" fill="#132a45" stroke="#2b4a76" stroke-width="6"/>
|
|
40
|
+
<path d="M10 70v300c0 25 67 46 150 46s150-21 150-46V70" fill="#0f2138" stroke="#2b4a76" stroke-width="6"/>
|
|
41
|
+
<path d="M10 170c0 25 67 46 150 46s150-21 150-46M10 270c0 25 67 46 150 46s150-21 150-46" fill="none" stroke="#2b4a76" stroke-width="6"/>
|
|
42
|
+
<ellipse cx="160" cy="70" rx="118" ry="30" fill="none" stroke="#22d3ee" stroke-width="6" opacity="0.8"/>
|
|
43
|
+
</g>
|
|
44
|
+
<path d="M1080 420h140" stroke="#475569" stroke-width="12" stroke-linecap="round" stroke-dasharray="4 24"/>
|
|
45
|
+
<path d="M1330 420h150" stroke="url(#accent)" stroke-width="12" stroke-linecap="round"/>
|
|
46
|
+
<path d="M1478 388l52 32-52 32z" fill="#6366f1"/>
|
|
47
|
+
<g transform="translate(1150 560)">
|
|
48
|
+
<path d="M20 46a42 42 0 0 1 84 0" fill="none" stroke="url(#accent)" stroke-width="12" stroke-linecap="round"/>
|
|
49
|
+
<rect x="0" y="44" width="124" height="70" rx="16" fill="#0f2138" stroke="url(#accent)" stroke-width="12"/>
|
|
50
|
+
<circle cx="62" cy="82" r="10" fill="#22d3ee"/>
|
|
51
|
+
</g>
|
|
52
|
+
<text x="800" y="790" fill="#f8fafc" font-family="sans-serif" font-size="82" font-weight="700" text-anchor="middle">TELEMETRY</text>
|
|
53
|
+
<text x="800" y="850" fill="#94a3b8" font-family="sans-serif" font-size="30" text-anchor="middle">local-first agent observability, opt-in OTLP export</text>
|
|
54
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator commands: setup, status, maintenance.
|
|
3
|
+
*
|
|
4
|
+
* Alisio namespaces external plugin commands as `<plugin id>:<name>`. This
|
|
5
|
+
* plugin registers the names below, so the real invocation form is
|
|
6
|
+
* `/telemetry:telemetry-setup`, `/telemetry:telemetry-status`,
|
|
7
|
+
* `/telemetry:telemetry-flush` and `/telemetry:telemetry-prune`.
|
|
8
|
+
*
|
|
9
|
+
* `telemetry-setup` persists configuration non-interactively and reports exactly
|
|
10
|
+
* which fields changed. It never echoes a credential: the OTLP token is only
|
|
11
|
+
* ever referenced by the name of the environment variable that holds it.
|
|
12
|
+
*/
|
|
13
|
+
import type { PluginAPI } from "@alisio/sdk";
|
|
14
|
+
import type { TelemetryRuntime } from "./runtime.js";
|
|
15
|
+
export declare const COMMAND_NAMES: readonly ["telemetry-setup", "telemetry-status", "telemetry-flush", "telemetry-prune"];
|
|
16
|
+
/** Replace a home-directory prefix with `~` so a path is not machine-specific. */
|
|
17
|
+
export declare function displayPath(path: string, home: string | undefined): string;
|
|
18
|
+
export declare function registerCommands(api: PluginAPI, runtime: TelemetryRuntime): void;
|
|
19
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAkB,SAAS,EAAE,MAAM,aAAa,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,eAAO,MAAM,aAAa,YACxB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,CACT,CAAC;AAiKX,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI1E;AAMD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA0JhF"}
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { ConfigError, createDefaultConfig, describeConfig, updateConfig, writeConfigFile, } from "./config.js";
|
|
2
|
+
import { sanitizeField } from "./format.js";
|
|
3
|
+
export const COMMAND_NAMES = [
|
|
4
|
+
"telemetry-setup",
|
|
5
|
+
"telemetry-status",
|
|
6
|
+
"telemetry-flush",
|
|
7
|
+
"telemetry-prune",
|
|
8
|
+
];
|
|
9
|
+
const SETUP_USAGE = [
|
|
10
|
+
"Usage: /telemetry:telemetry-setup [options]",
|
|
11
|
+
" --enable-remote | --disable-remote",
|
|
12
|
+
" --endpoint <url>",
|
|
13
|
+
" --token-env <ENV_VAR_NAME> (the value is never stored)",
|
|
14
|
+
" --service-name <name> --environment <name> --instance-id <id>",
|
|
15
|
+
" --sampling <0..1> --retention-days <n>",
|
|
16
|
+
" --capture <prompts,completions,tool-arguments,tool-results|none>",
|
|
17
|
+
" --redaction <strict|standard>",
|
|
18
|
+
" --flush-interval-ms <n> --batch-size <n> --otlp-batch-size <n>",
|
|
19
|
+
" --gzip | --no-gzip --json",
|
|
20
|
+
].join("\n");
|
|
21
|
+
function parseArguments(args) {
|
|
22
|
+
const tokens = args.trim() === "" ? [] : args.trim().split(/\s+/);
|
|
23
|
+
const values = new Map();
|
|
24
|
+
const flags = new Set();
|
|
25
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
26
|
+
const token = tokens[index];
|
|
27
|
+
if (!token.startsWith("--"))
|
|
28
|
+
return { values, flags };
|
|
29
|
+
const key = token.slice(2);
|
|
30
|
+
if (key === "")
|
|
31
|
+
return { values, flags };
|
|
32
|
+
if (["enable-remote", "disable-remote", "gzip", "no-gzip", "json"].includes(key)) {
|
|
33
|
+
flags.add(key);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const value = tokens[index + 1];
|
|
37
|
+
if (value === undefined || value.startsWith("--")) {
|
|
38
|
+
values.set(key, "");
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
values.set(key, value);
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
return { values, flags };
|
|
45
|
+
}
|
|
46
|
+
function integerValue(parsed, key) {
|
|
47
|
+
const raw = parsed.values.get(key);
|
|
48
|
+
if (raw === undefined || raw === "")
|
|
49
|
+
return undefined;
|
|
50
|
+
if (!/^-?\d+$/.test(raw))
|
|
51
|
+
throw new ConfigError(`--${key}: expected an integer`);
|
|
52
|
+
return Number(raw);
|
|
53
|
+
}
|
|
54
|
+
function ratioValue(parsed, key) {
|
|
55
|
+
const raw = parsed.values.get(key);
|
|
56
|
+
if (raw === undefined || raw === "")
|
|
57
|
+
return undefined;
|
|
58
|
+
const value = Number(raw);
|
|
59
|
+
if (!Number.isFinite(value) || value < 0 || value > 1)
|
|
60
|
+
throw new ConfigError(`--${key}: expected a number between 0 and 1`);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function captureValue(parsed) {
|
|
64
|
+
const raw = parsed.values.get("capture");
|
|
65
|
+
if (raw === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (raw === "none" || raw === "")
|
|
68
|
+
return { prompts: false, completions: false, toolArguments: false, toolResults: false };
|
|
69
|
+
const enabled = new Set(raw.split(",").map((value) => value.trim().toLowerCase()));
|
|
70
|
+
const known = ["prompts", "completions", "tool-arguments", "tool-results"];
|
|
71
|
+
for (const value of enabled) {
|
|
72
|
+
if (!known.includes(value))
|
|
73
|
+
throw new ConfigError(`--capture: unknown value "${value}"`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
prompts: enabled.has("prompts"),
|
|
77
|
+
completions: enabled.has("completions"),
|
|
78
|
+
toolArguments: enabled.has("tool-arguments"),
|
|
79
|
+
toolResults: enabled.has("tool-results"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function buildOverrides(parsed) {
|
|
83
|
+
const overrides = {};
|
|
84
|
+
if (parsed.flags.has("enable-remote") || parsed.flags.has("disable-remote")) {
|
|
85
|
+
overrides.otlp = {
|
|
86
|
+
...(overrides.otlp ?? {}),
|
|
87
|
+
enabled: parsed.flags.has("enable-remote"),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const endpoint = parsed.values.get("endpoint");
|
|
91
|
+
if (endpoint !== undefined && endpoint !== "") {
|
|
92
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), endpoint };
|
|
93
|
+
}
|
|
94
|
+
const tokenEnv = parsed.values.get("token-env");
|
|
95
|
+
if (tokenEnv !== undefined && tokenEnv !== "") {
|
|
96
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), tokenEnv };
|
|
97
|
+
}
|
|
98
|
+
const serviceName = parsed.values.get("service-name");
|
|
99
|
+
if (serviceName !== undefined && serviceName !== "") {
|
|
100
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), serviceName };
|
|
101
|
+
}
|
|
102
|
+
const environment = parsed.values.get("environment");
|
|
103
|
+
if (environment !== undefined) {
|
|
104
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), environment };
|
|
105
|
+
}
|
|
106
|
+
const instanceId = parsed.values.get("instance-id");
|
|
107
|
+
if (instanceId !== undefined) {
|
|
108
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), instanceId };
|
|
109
|
+
}
|
|
110
|
+
const sampling = ratioValue(parsed, "sampling");
|
|
111
|
+
if (sampling !== undefined)
|
|
112
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), samplingRatio: sampling };
|
|
113
|
+
if (parsed.flags.has("gzip") || parsed.flags.has("no-gzip")) {
|
|
114
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), gzip: parsed.flags.has("gzip") };
|
|
115
|
+
}
|
|
116
|
+
const otlpBatchSize = integerValue(parsed, "otlp-batch-size");
|
|
117
|
+
if (otlpBatchSize !== undefined)
|
|
118
|
+
overrides.otlp = { ...(overrides.otlp ?? {}), batchSize: otlpBatchSize };
|
|
119
|
+
const retentionDays = integerValue(parsed, "retention-days");
|
|
120
|
+
if (retentionDays !== undefined)
|
|
121
|
+
overrides.retentionDays = retentionDays;
|
|
122
|
+
const capture = captureValue(parsed);
|
|
123
|
+
if (capture !== undefined)
|
|
124
|
+
overrides.capture = capture;
|
|
125
|
+
const redaction = parsed.values.get("redaction");
|
|
126
|
+
if (redaction !== undefined && redaction !== "") {
|
|
127
|
+
if (redaction !== "strict" && redaction !== "standard")
|
|
128
|
+
throw new ConfigError("--redaction: expected strict or standard");
|
|
129
|
+
overrides.redaction = { mode: redaction };
|
|
130
|
+
}
|
|
131
|
+
const flushIntervalMs = integerValue(parsed, "flush-interval-ms");
|
|
132
|
+
if (flushIntervalMs !== undefined)
|
|
133
|
+
overrides.batch = { ...(overrides.batch ?? {}), flushIntervalMs };
|
|
134
|
+
const batchSize = integerValue(parsed, "batch-size");
|
|
135
|
+
if (batchSize !== undefined)
|
|
136
|
+
overrides.batch = { ...(overrides.batch ?? {}), batchSize };
|
|
137
|
+
return overrides;
|
|
138
|
+
}
|
|
139
|
+
/** Flatten a config to comparable dot-path primitives, without credentials. */
|
|
140
|
+
function flatten(value, prefix = "") {
|
|
141
|
+
if (value === null || typeof value !== "object")
|
|
142
|
+
return { [prefix]: JSON.stringify(value) };
|
|
143
|
+
const out = {};
|
|
144
|
+
for (const [key, item] of Object.entries(value)) {
|
|
145
|
+
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
146
|
+
Object.assign(out, flatten(item, path));
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function diffConfig(before, after) {
|
|
151
|
+
const left = flatten(before);
|
|
152
|
+
const right = flatten(after);
|
|
153
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
154
|
+
const changes = [];
|
|
155
|
+
for (const key of [...keys].sort()) {
|
|
156
|
+
if (left[key] !== right[key])
|
|
157
|
+
changes.push(`${key}: ${left[key] ?? "(unset)"} -> ${right[key] ?? "(unset)"}`);
|
|
158
|
+
}
|
|
159
|
+
return changes;
|
|
160
|
+
}
|
|
161
|
+
/** Replace a home-directory prefix with `~` so a path is not machine-specific. */
|
|
162
|
+
export function displayPath(path, home) {
|
|
163
|
+
if (home !== undefined && home !== "" && path.startsWith(home))
|
|
164
|
+
return `~${path.slice(home.length)}`;
|
|
165
|
+
return path;
|
|
166
|
+
}
|
|
167
|
+
function asJson(parsed) {
|
|
168
|
+
return parsed.flags.has("json");
|
|
169
|
+
}
|
|
170
|
+
export function registerCommands(api, runtime) {
|
|
171
|
+
const register = (name, description, argumentHint, handler) => {
|
|
172
|
+
api.commands.register(name, handler, { description, argumentHint });
|
|
173
|
+
};
|
|
174
|
+
register("telemetry-setup", "Persist telemetry configuration non-interactively", "[options]", async (args) => {
|
|
175
|
+
const parsed = parseArguments(args);
|
|
176
|
+
try {
|
|
177
|
+
const base = runtime.config ?? createDefaultConfig();
|
|
178
|
+
const overrides = buildOverrides(parsed);
|
|
179
|
+
const next = updateConfig(base, overrides);
|
|
180
|
+
writeConfigFile(runtime.paths.configFile, next);
|
|
181
|
+
const before = runtime.config ? describeConfig(runtime.config, runtime.env) : null;
|
|
182
|
+
const after = describeConfig(next, runtime.env);
|
|
183
|
+
const changes = before === null ? ["configuration file created"] : diffConfig(before, after);
|
|
184
|
+
const payload = {
|
|
185
|
+
saved: true,
|
|
186
|
+
configFile: displayPath(runtime.paths.configFile, runtime.env.HOME),
|
|
187
|
+
changed: changes,
|
|
188
|
+
restartRequired: true,
|
|
189
|
+
note: "The OTLP token is never stored. It is read at export time from the environment variable named below.",
|
|
190
|
+
tokenEnv: next.otlp.tokenEnv,
|
|
191
|
+
};
|
|
192
|
+
if (asJson(parsed))
|
|
193
|
+
return JSON.stringify(payload);
|
|
194
|
+
return [
|
|
195
|
+
`Telemetry configuration saved to ${payload.configFile}.`,
|
|
196
|
+
changes.length === 0 ? "No fields changed." : "Changed:",
|
|
197
|
+
...changes.map((change) => ` ${change}`),
|
|
198
|
+
`Token environment variable: ${payload.tokenEnv} (value never stored).`,
|
|
199
|
+
"Restart Alisio to apply the new settings.",
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const message = error instanceof ConfigError ? error.message : "configuration could not be saved";
|
|
204
|
+
return asJson(parsed)
|
|
205
|
+
? JSON.stringify({ saved: false, error: message })
|
|
206
|
+
: `Telemetry setup failed: ${message}\n${SETUP_USAGE}`;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
register("telemetry-status", "Show effective telemetry configuration and local store stats", "", async (args) => {
|
|
210
|
+
const parsed = parseArguments(args);
|
|
211
|
+
const counts = runtime.store?.counts() ?? null;
|
|
212
|
+
const payload = {
|
|
213
|
+
enabled: runtime.store !== null,
|
|
214
|
+
error: runtime.configError,
|
|
215
|
+
config: runtime.config ? describeConfig(runtime.config, runtime.env) : null,
|
|
216
|
+
database: displayPath(runtime.paths.database, runtime.env.HOME),
|
|
217
|
+
configFile: displayPath(runtime.paths.configFile, runtime.env.HOME),
|
|
218
|
+
remoteExport: runtime.exporter !== null,
|
|
219
|
+
counts,
|
|
220
|
+
};
|
|
221
|
+
if (asJson(parsed))
|
|
222
|
+
return JSON.stringify(payload);
|
|
223
|
+
if (runtime.configError !== null) {
|
|
224
|
+
return `Telemetry is disabled: ${runtime.configError}\nConfig file: ${payload.configFile}\nDatabase: ${payload.database}`;
|
|
225
|
+
}
|
|
226
|
+
const config = payload.config ?? {};
|
|
227
|
+
return [
|
|
228
|
+
"Telemetry status",
|
|
229
|
+
` local store: ${runtime.store === null ? "unavailable" : "open"}`,
|
|
230
|
+
` remote export: ${payload.remoteExport ? "enabled" : "disabled (local-only)"}`,
|
|
231
|
+
` database: ${payload.database}`,
|
|
232
|
+
` config file: ${payload.configFile}`,
|
|
233
|
+
` retention: ${config.retentionDays ?? "-"} day(s)`,
|
|
234
|
+
` counts: ${counts ? `${counts.runs} run(s), ${counts.turns} turn(s), ${counts.toolCalls} tool call(s), ${counts.content} content row(s)` : "-"}`,
|
|
235
|
+
` batches: ${counts ? `${counts.pendingBatches} pending, ${counts.deadBatches} dead` : "-"}`,
|
|
236
|
+
" content capture is metadata-only unless explicitly enabled; the OTLP token is never stored.",
|
|
237
|
+
].join("\n");
|
|
238
|
+
});
|
|
239
|
+
register("telemetry-flush", "Flush queued records and attempt remote export", "", async (args) => {
|
|
240
|
+
const parsed = parseArguments(args);
|
|
241
|
+
const report = await runtime.flush();
|
|
242
|
+
const payload = {
|
|
243
|
+
records: report.records,
|
|
244
|
+
content: report.content,
|
|
245
|
+
dropped: report.dropped,
|
|
246
|
+
queueSize: report.queueSize,
|
|
247
|
+
optimized: report.optimized,
|
|
248
|
+
error: report.error,
|
|
249
|
+
exports: report.exports.map((result) => ({
|
|
250
|
+
signal: result.signal,
|
|
251
|
+
status: result.status,
|
|
252
|
+
attempts: result.attempts,
|
|
253
|
+
httpStatus: result.httpStatus,
|
|
254
|
+
error: sanitizeField(result.error ?? "", 200),
|
|
255
|
+
})),
|
|
256
|
+
};
|
|
257
|
+
if (asJson(parsed))
|
|
258
|
+
return JSON.stringify(payload);
|
|
259
|
+
const lines = [
|
|
260
|
+
`Flushed ${report.records} record(s) (${report.content} content row(s)) to the local store.`,
|
|
261
|
+
`Queue: ${report.queueSize} pending, ${report.dropped} dropped since start.`,
|
|
262
|
+
report.error === null ? "No flush errors." : `Flush error: ${report.error}`,
|
|
263
|
+
];
|
|
264
|
+
if (payload.exports.length === 0)
|
|
265
|
+
lines.push("Remote export is disabled (local-only); no network call was made.");
|
|
266
|
+
else
|
|
267
|
+
for (const result of payload.exports)
|
|
268
|
+
lines.push(` ${result.signal}: ${result.status} after ${result.attempts} attempt(s)${result.httpStatus === null ? "" : ` (HTTP ${result.httpStatus})`}`);
|
|
269
|
+
return lines.join("\n");
|
|
270
|
+
});
|
|
271
|
+
register("telemetry-prune", "Delete telemetry older than the retention window", "[--days <n>] [--json]", async (args) => {
|
|
272
|
+
const parsed = parseArguments(args);
|
|
273
|
+
const days = integerValue(parsed, "days");
|
|
274
|
+
try {
|
|
275
|
+
const report = runtime.prune(days);
|
|
276
|
+
const payload = {
|
|
277
|
+
cutoff: report.cutoff,
|
|
278
|
+
retentionDays: report.retentionDays,
|
|
279
|
+
deleted: report.deleted,
|
|
280
|
+
note: report.note,
|
|
281
|
+
};
|
|
282
|
+
if (asJson(parsed))
|
|
283
|
+
return JSON.stringify(payload);
|
|
284
|
+
return [
|
|
285
|
+
`Pruned telemetry older than ${report.retentionDays} day(s) (before ${report.cutoff}).`,
|
|
286
|
+
`Deleted ${report.deleted.runs} run(s), ${report.deleted.turns} turn(s), ${report.deleted.toolCalls} tool call(s), ${report.deleted.content} content row(s).`,
|
|
287
|
+
report.note,
|
|
288
|
+
].join("\n");
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const message = error instanceof ConfigError ? error.message : "prune failed";
|
|
292
|
+
return asJson(parsed)
|
|
293
|
+
? JSON.stringify({ pruned: false, error: message })
|
|
294
|
+
: `Telemetry prune failed: ${message}`;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=commands.js.map
|