@respan/cli 0.13.2 → 0.14.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.
@@ -0,0 +1,135 @@
1
+ /** otelcol-contrib release the config is validated against (collector/config.yaml). */
2
+ export declare const COLLECTOR_VERSION = "0.160.0";
3
+ /** Upstream contrib image used by `respan collector start --docker`. */
4
+ export declare const COLLECTOR_IMAGE = "otel/opentelemetry-collector-contrib:0.160.0";
5
+ /** OTLP/HTTP receiver port — what the SDK's RESPAN_BASE_URL points at. */
6
+ export declare const DEFAULT_COLLECTOR_PORT = 4318;
7
+ /** health_check extension port (fixed in the config unless overridden by env). */
8
+ export declare const DEFAULT_HEALTH_PORT = 13133;
9
+ /** Prometheus metrics port (hard-coded in the config). */
10
+ export declare const DEFAULT_METRICS_PORT = 8888;
11
+ /** file_storage max_size default: 512 MiB. */
12
+ export declare const DEFAULT_MAX_SIZE_BYTES = 536870912;
13
+ /** sending_queue.queue_size default (batches). */
14
+ export declare const DEFAULT_QUEUE_SIZE = 10000;
15
+ export declare const CONTAINER_NAME = "respan-collector";
16
+ export declare const VOLUME_NAME = "respan-collector-queue";
17
+ /** Queue directory inside the container (matches collector/Dockerfile). */
18
+ export declare const CONTAINER_DATA_DIR = "/var/lib/otelcol/respan";
19
+ /** Config mount point inside the container (the upstream image's default path). */
20
+ export declare const CONTAINER_CONFIG_PATH = "/etc/otelcol-contrib/config.yaml";
21
+ /** Uid the upstream contrib image runs as; the queue volume must be writable by it. */
22
+ export declare const CONTAINER_UID = "10001";
23
+ /** Helper image used once to chown a fresh queue volume (the contrib image has no shell). */
24
+ export declare const VOLUME_INIT_IMAGE = "alpine:3.20";
25
+ /**
26
+ * The collector configuration — an exact copy of collector/config.yaml (the
27
+ * single source of truth; tests/collector.test.mjs asserts they never drift).
28
+ * Every tunable is a `${env:NAME:-default}` placeholder; the API key is only
29
+ * ever read from the RESPAN_API_KEY environment variable.
30
+ */
31
+ export declare const COLLECTOR_CONFIG_TEMPLATE = "# Respan Collector \u2014 an OpenTelemetry Collector configuration.\n#\n# Receives OTLP/HTTP from the Respan SDKs on localhost and forwards it to the\n# Respan ingest endpoint through a bounded, persistent (write-ahead-log) queue,\n# so spans survive Respan outages and collector restarts without unbounded\n# local growth. Every value can be overridden with an environment variable.\n#\n# Validated against otelcol-contrib 0.160.0.\n\nextensions:\n file_storage:\n # Persistent queue. Entries are deleted as soon as Respan acknowledges them;\n # in steady state the directory is nearly empty.\n directory: ${env:RESPAN_COLLECTOR_DATA_DIR:-/var/lib/otelcol/respan}\n create_directory: true\n # Hard cap on disk (bytes). When reached, new batches are dropped and counted\n # in otelcol_exporter_enqueue_failed_spans instead of filling the disk.\n max_size: ${env:RESPAN_COLLECTOR_MAX_SIZE_BYTES:-536870912}\n timeout: 1s\n compaction:\n on_start: true\n on_rebound: true\n directory: ${env:RESPAN_COLLECTOR_DATA_DIR:-/var/lib/otelcol/respan}\n health_check:\n endpoint: ${env:RESPAN_COLLECTOR_HEALTH_ENDPOINT:-127.0.0.1:13133}\n\nreceivers:\n otlp:\n protocols:\n http:\n endpoint: ${env:RESPAN_COLLECTOR_LISTEN:-127.0.0.1:4318}\n # The Respan SDKs post to <RESPAN_BASE_URL>/api/v2/traces.\n traces_url_path: /api/v2/traces\n\nprocessors:\n memory_limiter:\n check_interval: 1s\n limit_mib: ${env:RESPAN_COLLECTOR_MEMORY_LIMIT_MIB:-512}\n spike_limit_mib: 128\n batch:\n send_batch_size: 512\n send_batch_max_size: 1024\n timeout: 2s\n\nexporters:\n otlphttp/respan:\n traces_endpoint: ${env:RESPAN_COLLECTOR_EXPORT_URL:-https://api.respan.ai/api/v2/traces}\n encoding: json\n # Respan's ingest does not decompress request bodies; keep this `none` unless\n # your endpoint (e.g. a proxy in front of a self-hosted deployment) accepts gzip.\n compression: ${env:RESPAN_COLLECTOR_COMPRESSION:-none}\n headers:\n Authorization: \"Bearer ${env:RESPAN_API_KEY}\"\n timeout: 30s\n sending_queue:\n enabled: true\n storage: file_storage\n # Number of batches kept while Respan is unreachable (also bounded by max_size).\n queue_size: ${env:RESPAN_COLLECTOR_QUEUE_SIZE:-10000}\n num_consumers: 4\n retry_on_failure:\n enabled: true\n initial_interval: 5s\n max_interval: 60s\n # Never give up on a batch; the queue bounds are the only limit.\n max_elapsed_time: 0\n\nservice:\n extensions: [file_storage, health_check]\n telemetry:\n metrics:\n level: normal\n readers:\n - pull:\n exporter:\n prometheus:\n host: ${env:RESPAN_COLLECTOR_METRICS_HOST:-127.0.0.1}\n port: 8888\n pipelines:\n traces:\n receivers: [otlp]\n processors: [memory_limiter, batch]\n exporters: [otlphttp/respan]\n";
32
+ export interface CollectorConfigOptions {
33
+ /** Full ingest URL, e.g. https://api.respan.ai/api/v2/traces (RESPAN_COLLECTOR_EXPORT_URL). */
34
+ exportUrl?: string;
35
+ /** Persistent queue directory (RESPAN_COLLECTOR_DATA_DIR). */
36
+ dataDir?: string;
37
+ /** Disk cap for the queue in bytes (RESPAN_COLLECTOR_MAX_SIZE_BYTES). */
38
+ maxSizeBytes?: number;
39
+ /** Batches kept while Respan is unreachable (RESPAN_COLLECTOR_QUEUE_SIZE). */
40
+ queueSize?: number;
41
+ /** OTLP/HTTP listen address, e.g. 127.0.0.1:4318 (RESPAN_COLLECTOR_LISTEN). */
42
+ listen?: string;
43
+ /** health_check listen address (RESPAN_COLLECTOR_HEALTH_ENDPOINT). */
44
+ healthEndpoint?: string;
45
+ /** Prometheus metrics bind host (RESPAN_COLLECTOR_METRICS_HOST). */
46
+ metricsHost?: string;
47
+ /** memory_limiter limit (RESPAN_COLLECTOR_MEMORY_LIMIT_MIB). */
48
+ memoryLimitMib?: number;
49
+ }
50
+ /**
51
+ * Render the collector config. Each `${env:NAME:-default}` placeholder whose
52
+ * option is given is replaced by that value; the others keep the placeholder
53
+ * (and therefore stay overridable through the environment). RESPAN_API_KEY is
54
+ * never substituted — the file must not contain the secret.
55
+ */
56
+ export declare function buildCollectorConfig(opts?: CollectorConfigOptions): string;
57
+ export interface CollectorPaths {
58
+ /** ~/.respan/collector */
59
+ root: string;
60
+ configPath: string;
61
+ dataDir: string;
62
+ binDir: string;
63
+ binaryPath: string;
64
+ pidPath: string;
65
+ logPath: string;
66
+ }
67
+ /** Where the CLI keeps the collector config, queue, binary, pid and log. */
68
+ export declare function collectorPaths(home?: string): CollectorPaths;
69
+ /**
70
+ * Ingest URL for a Respan API base URL. Mirrors the SDKs, which accept both
71
+ * `https://api.respan.ai` and `https://api.respan.ai/api` and post to
72
+ * `<origin>/api/v2/traces`.
73
+ */
74
+ export declare function exportUrlFromBaseUrl(baseUrl: string): string;
75
+ /** Base URL the SDKs / pi extension should use to reach a local collector. */
76
+ export declare function collectorBaseUrl(port?: number): string;
77
+ export interface DockerRunOptions {
78
+ /** Host path of the rendered config (mounted read-only). */
79
+ configPath: string;
80
+ /** Host port published on 127.0.0.1 for the OTLP/HTTP receiver. */
81
+ port: number;
82
+ exportUrl: string;
83
+ maxSizeBytes: number;
84
+ queueSize: number;
85
+ image?: string;
86
+ }
87
+ /**
88
+ * Arguments for `docker <args>` that start the collector container. All ports
89
+ * are published on 127.0.0.1 only. RESPAN_API_KEY is passed through from the
90
+ * caller's environment (`-e RESPAN_API_KEY` without a value) so the secret
91
+ * never appears on the command line.
92
+ */
93
+ export declare function dockerRunArgs(opts: DockerRunOptions): string[];
94
+ /**
95
+ * One-off `docker <args>` that makes a fresh queue volume writable by the
96
+ * collector. Docker creates a new named volume owned by root, and the upstream
97
+ * image runs as uid 10001 without a shell, so a helper image does the chown
98
+ * (the same trick collector/Dockerfile uses at build time).
99
+ */
100
+ export declare function dockerVolumeInitArgs(volume?: string): string[];
101
+ /** Release tarball for otelcol-contrib (contains `otelcol-contrib` and README.md). */
102
+ export declare function binaryDownloadUrl(version?: string, platform?: string, arch?: string): string;
103
+ /**
104
+ * SHA-256 of the tarball. The release publishes one `<asset>.sha256` file per
105
+ * asset (there is no aggregate checksums file); its body is the bare hex digest.
106
+ */
107
+ export declare function checksumUrl(version?: string, platform?: string, arch?: string): string;
108
+ /** Extract the hex digest from a `.sha256` body (`<hex>` or `<hex> <file>`). */
109
+ export declare function parseSha256(text: string): string | undefined;
110
+ /**
111
+ * Parse Prometheus text exposition into metric name → value. Labels are
112
+ * stripped; series that share a name are summed.
113
+ */
114
+ export declare function parsePrometheusText(text: string): Map<string, number>;
115
+ export interface CollectorMetricsSummary {
116
+ /** Batches currently waiting in the persistent queue. */
117
+ queueSize: number;
118
+ /** sending_queue.queue_size as reported by the exporter. */
119
+ queueCapacity: number;
120
+ /** Spans dropped because the queue (or its disk cap) was full. */
121
+ enqueueFailedSpans: number;
122
+ /** Spans given up on after export failures (permanent errors such as 4xx). */
123
+ sendFailedSpans: number;
124
+ /** Spans acknowledged by Respan. */
125
+ sentSpans: number;
126
+ }
127
+ /**
128
+ * Pick the exporter metrics out of a scrape. otelcol-contrib 0.160.0 exposes
129
+ * them without a `_total` suffix (e.g. `otelcol_exporter_sent_spans`); the
130
+ * suffixed spelling is accepted too in case a build re-enables it. Counters
131
+ * only appear once they have been incremented, so a missing one reads as 0.
132
+ */
133
+ export declare function summarizeCollectorMetrics(metrics: Map<string, number>): CollectorMetricsSummary;
134
+ /** Human-readable byte count (1024-based). */
135
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,289 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ // Pure helpers for `respan collector ...`. Kept free of process / network side
4
+ // effects so they can be unit-tested directly (tests/collector.test.mjs); the
5
+ // commands and lib/collector-runtime.ts do the spawning and polling.
6
+ /** otelcol-contrib release the config is validated against (collector/config.yaml). */
7
+ export const COLLECTOR_VERSION = '0.160.0';
8
+ /** Upstream contrib image used by `respan collector start --docker`. */
9
+ export const COLLECTOR_IMAGE = `otel/opentelemetry-collector-contrib:${COLLECTOR_VERSION}`;
10
+ /** OTLP/HTTP receiver port — what the SDK's RESPAN_BASE_URL points at. */
11
+ export const DEFAULT_COLLECTOR_PORT = 4318;
12
+ /** health_check extension port (fixed in the config unless overridden by env). */
13
+ export const DEFAULT_HEALTH_PORT = 13133;
14
+ /** Prometheus metrics port (hard-coded in the config). */
15
+ export const DEFAULT_METRICS_PORT = 8888;
16
+ /** file_storage max_size default: 512 MiB. */
17
+ export const DEFAULT_MAX_SIZE_BYTES = 536870912;
18
+ /** sending_queue.queue_size default (batches). */
19
+ export const DEFAULT_QUEUE_SIZE = 10000;
20
+ export const CONTAINER_NAME = 'respan-collector';
21
+ export const VOLUME_NAME = 'respan-collector-queue';
22
+ /** Queue directory inside the container (matches collector/Dockerfile). */
23
+ export const CONTAINER_DATA_DIR = '/var/lib/otelcol/respan';
24
+ /** Config mount point inside the container (the upstream image's default path). */
25
+ export const CONTAINER_CONFIG_PATH = '/etc/otelcol-contrib/config.yaml';
26
+ /** Uid the upstream contrib image runs as; the queue volume must be writable by it. */
27
+ export const CONTAINER_UID = '10001';
28
+ /** Helper image used once to chown a fresh queue volume (the contrib image has no shell). */
29
+ export const VOLUME_INIT_IMAGE = 'alpine:3.20';
30
+ /**
31
+ * The collector configuration — an exact copy of collector/config.yaml (the
32
+ * single source of truth; tests/collector.test.mjs asserts they never drift).
33
+ * Every tunable is a `${env:NAME:-default}` placeholder; the API key is only
34
+ * ever read from the RESPAN_API_KEY environment variable.
35
+ */
36
+ export const COLLECTOR_CONFIG_TEMPLATE = `# Respan Collector — an OpenTelemetry Collector configuration.
37
+ #
38
+ # Receives OTLP/HTTP from the Respan SDKs on localhost and forwards it to the
39
+ # Respan ingest endpoint through a bounded, persistent (write-ahead-log) queue,
40
+ # so spans survive Respan outages and collector restarts without unbounded
41
+ # local growth. Every value can be overridden with an environment variable.
42
+ #
43
+ # Validated against otelcol-contrib 0.160.0.
44
+
45
+ extensions:
46
+ file_storage:
47
+ # Persistent queue. Entries are deleted as soon as Respan acknowledges them;
48
+ # in steady state the directory is nearly empty.
49
+ directory: \${env:RESPAN_COLLECTOR_DATA_DIR:-/var/lib/otelcol/respan}
50
+ create_directory: true
51
+ # Hard cap on disk (bytes). When reached, new batches are dropped and counted
52
+ # in otelcol_exporter_enqueue_failed_spans instead of filling the disk.
53
+ max_size: \${env:RESPAN_COLLECTOR_MAX_SIZE_BYTES:-536870912}
54
+ timeout: 1s
55
+ compaction:
56
+ on_start: true
57
+ on_rebound: true
58
+ directory: \${env:RESPAN_COLLECTOR_DATA_DIR:-/var/lib/otelcol/respan}
59
+ health_check:
60
+ endpoint: \${env:RESPAN_COLLECTOR_HEALTH_ENDPOINT:-127.0.0.1:13133}
61
+
62
+ receivers:
63
+ otlp:
64
+ protocols:
65
+ http:
66
+ endpoint: \${env:RESPAN_COLLECTOR_LISTEN:-127.0.0.1:4318}
67
+ # The Respan SDKs post to <RESPAN_BASE_URL>/api/v2/traces.
68
+ traces_url_path: /api/v2/traces
69
+
70
+ processors:
71
+ memory_limiter:
72
+ check_interval: 1s
73
+ limit_mib: \${env:RESPAN_COLLECTOR_MEMORY_LIMIT_MIB:-512}
74
+ spike_limit_mib: 128
75
+ batch:
76
+ send_batch_size: 512
77
+ send_batch_max_size: 1024
78
+ timeout: 2s
79
+
80
+ exporters:
81
+ otlphttp/respan:
82
+ traces_endpoint: \${env:RESPAN_COLLECTOR_EXPORT_URL:-https://api.respan.ai/api/v2/traces}
83
+ encoding: json
84
+ # Respan's ingest does not decompress request bodies; keep this \`none\` unless
85
+ # your endpoint (e.g. a proxy in front of a self-hosted deployment) accepts gzip.
86
+ compression: \${env:RESPAN_COLLECTOR_COMPRESSION:-none}
87
+ headers:
88
+ Authorization: "Bearer \${env:RESPAN_API_KEY}"
89
+ timeout: 30s
90
+ sending_queue:
91
+ enabled: true
92
+ storage: file_storage
93
+ # Number of batches kept while Respan is unreachable (also bounded by max_size).
94
+ queue_size: \${env:RESPAN_COLLECTOR_QUEUE_SIZE:-10000}
95
+ num_consumers: 4
96
+ retry_on_failure:
97
+ enabled: true
98
+ initial_interval: 5s
99
+ max_interval: 60s
100
+ # Never give up on a batch; the queue bounds are the only limit.
101
+ max_elapsed_time: 0
102
+
103
+ service:
104
+ extensions: [file_storage, health_check]
105
+ telemetry:
106
+ metrics:
107
+ level: normal
108
+ readers:
109
+ - pull:
110
+ exporter:
111
+ prometheus:
112
+ host: \${env:RESPAN_COLLECTOR_METRICS_HOST:-127.0.0.1}
113
+ port: 8888
114
+ pipelines:
115
+ traces:
116
+ receivers: [otlp]
117
+ processors: [memory_limiter, batch]
118
+ exporters: [otlphttp/respan]
119
+ `;
120
+ const CONFIG_ENV_VARS = {
121
+ exportUrl: 'RESPAN_COLLECTOR_EXPORT_URL',
122
+ dataDir: 'RESPAN_COLLECTOR_DATA_DIR',
123
+ maxSizeBytes: 'RESPAN_COLLECTOR_MAX_SIZE_BYTES',
124
+ queueSize: 'RESPAN_COLLECTOR_QUEUE_SIZE',
125
+ listen: 'RESPAN_COLLECTOR_LISTEN',
126
+ healthEndpoint: 'RESPAN_COLLECTOR_HEALTH_ENDPOINT',
127
+ metricsHost: 'RESPAN_COLLECTOR_METRICS_HOST',
128
+ memoryLimitMib: 'RESPAN_COLLECTOR_MEMORY_LIMIT_MIB',
129
+ };
130
+ /**
131
+ * Render the collector config. Each `${env:NAME:-default}` placeholder whose
132
+ * option is given is replaced by that value; the others keep the placeholder
133
+ * (and therefore stay overridable through the environment). RESPAN_API_KEY is
134
+ * never substituted — the file must not contain the secret.
135
+ */
136
+ export function buildCollectorConfig(opts = {}) {
137
+ const values = new Map();
138
+ for (const [key, envVar] of Object.entries(CONFIG_ENV_VARS)) {
139
+ const value = opts[key];
140
+ if (value !== undefined && value !== null && String(value) !== '') {
141
+ values.set(envVar, String(value));
142
+ }
143
+ }
144
+ return COLLECTOR_CONFIG_TEMPLATE.replace(/\$\{env:([A-Z0-9_]+):-[^}]*\}/g, (placeholder, name) => values.get(name) ?? placeholder);
145
+ }
146
+ /** Where the CLI keeps the collector config, queue, binary, pid and log. */
147
+ export function collectorPaths(home = os.homedir()) {
148
+ const root = path.join(home, '.respan', 'collector');
149
+ return {
150
+ root,
151
+ configPath: path.join(root, 'config.yaml'),
152
+ dataDir: path.join(root, 'data'),
153
+ binDir: path.join(root, 'bin'),
154
+ binaryPath: path.join(root, 'bin', 'otelcol-contrib'),
155
+ pidPath: path.join(root, 'collector.pid'),
156
+ logPath: path.join(root, 'collector.log'),
157
+ };
158
+ }
159
+ /**
160
+ * Ingest URL for a Respan API base URL. Mirrors the SDKs, which accept both
161
+ * `https://api.respan.ai` and `https://api.respan.ai/api` and post to
162
+ * `<origin>/api/v2/traces`.
163
+ */
164
+ export function exportUrlFromBaseUrl(baseUrl) {
165
+ const origin = baseUrl.trim().replace(/\/+$/, '').replace(/\/api$/, '').replace(/\/+$/, '');
166
+ return `${origin}/api/v2/traces`;
167
+ }
168
+ /** Base URL the SDKs / pi extension should use to reach a local collector. */
169
+ export function collectorBaseUrl(port = DEFAULT_COLLECTOR_PORT) {
170
+ return `http://127.0.0.1:${port}`;
171
+ }
172
+ /**
173
+ * Arguments for `docker <args>` that start the collector container. All ports
174
+ * are published on 127.0.0.1 only. RESPAN_API_KEY is passed through from the
175
+ * caller's environment (`-e RESPAN_API_KEY` without a value) so the secret
176
+ * never appears on the command line.
177
+ */
178
+ export function dockerRunArgs(opts) {
179
+ return [
180
+ 'run',
181
+ '-d',
182
+ '--name', CONTAINER_NAME,
183
+ '--restart', 'unless-stopped',
184
+ '-p', `127.0.0.1:${opts.port}:${DEFAULT_COLLECTOR_PORT}`,
185
+ '-p', `127.0.0.1:${DEFAULT_HEALTH_PORT}:${DEFAULT_HEALTH_PORT}`,
186
+ '-p', `127.0.0.1:${DEFAULT_METRICS_PORT}:${DEFAULT_METRICS_PORT}`,
187
+ '-e', 'RESPAN_API_KEY',
188
+ '-e', `RESPAN_COLLECTOR_EXPORT_URL=${opts.exportUrl}`,
189
+ '-e', `RESPAN_COLLECTOR_MAX_SIZE_BYTES=${opts.maxSizeBytes}`,
190
+ '-e', `RESPAN_COLLECTOR_QUEUE_SIZE=${opts.queueSize}`,
191
+ '-e', `RESPAN_COLLECTOR_LISTEN=0.0.0.0:${DEFAULT_COLLECTOR_PORT}`,
192
+ '-e', `RESPAN_COLLECTOR_HEALTH_ENDPOINT=0.0.0.0:${DEFAULT_HEALTH_PORT}`,
193
+ '-e', 'RESPAN_COLLECTOR_METRICS_HOST=0.0.0.0',
194
+ '-e', `RESPAN_COLLECTOR_DATA_DIR=${CONTAINER_DATA_DIR}`,
195
+ '-v', `${opts.configPath}:${CONTAINER_CONFIG_PATH}:ro`,
196
+ '-v', `${VOLUME_NAME}:${CONTAINER_DATA_DIR}`,
197
+ opts.image ?? COLLECTOR_IMAGE,
198
+ '--config', CONTAINER_CONFIG_PATH,
199
+ ];
200
+ }
201
+ /**
202
+ * One-off `docker <args>` that makes a fresh queue volume writable by the
203
+ * collector. Docker creates a new named volume owned by root, and the upstream
204
+ * image runs as uid 10001 without a shell, so a helper image does the chown
205
+ * (the same trick collector/Dockerfile uses at build time).
206
+ */
207
+ export function dockerVolumeInitArgs(volume = VOLUME_NAME) {
208
+ return [
209
+ 'run', '--rm',
210
+ '-v', `${volume}:/queue`,
211
+ VOLUME_INIT_IMAGE,
212
+ 'chown', `${CONTAINER_UID}:${CONTAINER_UID}`, '/queue',
213
+ ];
214
+ }
215
+ const RELEASES_BASE = 'https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download';
216
+ const RELEASE_OS = { darwin: 'darwin', linux: 'linux' };
217
+ const RELEASE_ARCH = { arm64: 'arm64', x64: 'amd64' };
218
+ /** Release tarball for otelcol-contrib (contains `otelcol-contrib` and README.md). */
219
+ export function binaryDownloadUrl(version = COLLECTOR_VERSION, platform = process.platform, arch = process.arch) {
220
+ const os = RELEASE_OS[platform];
221
+ const cpu = RELEASE_ARCH[arch];
222
+ if (!os || !cpu) {
223
+ throw new Error(`No otelcol-contrib binary for ${platform}/${arch} (supported: darwin/linux on arm64/x64). ` +
224
+ 'Use `respan collector start --docker` or run the collector another way.');
225
+ }
226
+ return `${RELEASES_BASE}/v${version}/otelcol-contrib_${version}_${os}_${cpu}.tar.gz`;
227
+ }
228
+ /**
229
+ * SHA-256 of the tarball. The release publishes one `<asset>.sha256` file per
230
+ * asset (there is no aggregate checksums file); its body is the bare hex digest.
231
+ */
232
+ export function checksumUrl(version = COLLECTOR_VERSION, platform = process.platform, arch = process.arch) {
233
+ return `${binaryDownloadUrl(version, platform, arch)}.sha256`;
234
+ }
235
+ /** Extract the hex digest from a `.sha256` body (`<hex>` or `<hex> <file>`). */
236
+ export function parseSha256(text) {
237
+ return text.match(/\b[0-9a-fA-F]{64}\b/)?.[0].toLowerCase();
238
+ }
239
+ /**
240
+ * Parse Prometheus text exposition into metric name → value. Labels are
241
+ * stripped; series that share a name are summed.
242
+ */
243
+ export function parsePrometheusText(text) {
244
+ const metrics = new Map();
245
+ for (const rawLine of text.split('\n')) {
246
+ const line = rawLine.trim();
247
+ if (!line || line.startsWith('#'))
248
+ continue;
249
+ const match = line.match(/^([A-Za-z_:][A-Za-z0-9_:]*)(?:\{.*\})?\s+(\S+)/);
250
+ if (!match)
251
+ continue;
252
+ const value = Number(match[2]);
253
+ if (!Number.isFinite(value))
254
+ continue;
255
+ metrics.set(match[1], (metrics.get(match[1]) ?? 0) + value);
256
+ }
257
+ return metrics;
258
+ }
259
+ /**
260
+ * Pick the exporter metrics out of a scrape. otelcol-contrib 0.160.0 exposes
261
+ * them without a `_total` suffix (e.g. `otelcol_exporter_sent_spans`); the
262
+ * suffixed spelling is accepted too in case a build re-enables it. Counters
263
+ * only appear once they have been incremented, so a missing one reads as 0.
264
+ */
265
+ export function summarizeCollectorMetrics(metrics) {
266
+ const get = (name) => metrics.get(name) ?? metrics.get(`${name}_total`) ?? 0;
267
+ return {
268
+ queueSize: get('otelcol_exporter_queue_size'),
269
+ queueCapacity: get('otelcol_exporter_queue_capacity'),
270
+ enqueueFailedSpans: get('otelcol_exporter_enqueue_failed_spans'),
271
+ sendFailedSpans: get('otelcol_exporter_send_failed_spans'),
272
+ sentSpans: get('otelcol_exporter_sent_spans'),
273
+ };
274
+ }
275
+ /** Human-readable byte count (1024-based). */
276
+ export function formatBytes(bytes) {
277
+ if (!Number.isFinite(bytes) || bytes < 0)
278
+ return '-';
279
+ if (bytes < 1024)
280
+ return `${bytes} B`;
281
+ const units = ['KiB', 'MiB', 'GiB', 'TiB'];
282
+ let value = bytes / 1024;
283
+ let unit = 0;
284
+ while (value >= 1024 && unit < units.length - 1) {
285
+ value /= 1024;
286
+ unit += 1;
287
+ }
288
+ return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
289
+ }
@@ -0,0 +1,49 @@
1
+ import { Scope } from './integrate.js';
2
+ /** npm package that ships the Respan pi extension. */
3
+ export declare const PI_PACKAGE_NAME = "@respan/instrumentation-pi";
4
+ /** Package spec understood by `pi install` / `pi remove`. */
5
+ export declare const PI_PACKAGE_SPEC = "npm:@respan/instrumentation-pi";
6
+ /**
7
+ * Arguments for the `pi` binary that install the extension package.
8
+ *
9
+ * - global (default): `pi install npm:@respan/instrumentation-pi`
10
+ * - local: `pi install -l npm:@respan/instrumentation-pi`
11
+ */
12
+ export declare function piInstallArgs(scope: Scope): string[];
13
+ /**
14
+ * Location of the non-secret config file read by the extension:
15
+ * `~/.pi/agent/respan.json` (global) or `<projectRoot>/.pi/respan.json` (local).
16
+ * The extension applies defaults → global → project → env, so both may coexist.
17
+ */
18
+ export declare function piConfigPath(scope: Scope, projectRoot: string, home?: string): string;
19
+ /** `run` = one trace per agent run (default); `session` = one multi-root trace per pi session. */
20
+ export type PiTraceScope = 'run' | 'session';
21
+ export interface PiRespanConfigOptions {
22
+ enabled: boolean;
23
+ customerId?: string;
24
+ workflowName?: string;
25
+ projectId?: string;
26
+ /** Written as `trace_scope`. */
27
+ traceScope?: PiTraceScope;
28
+ /** Only written when it differs from the SaaS default. */
29
+ baseUrl?: string;
30
+ /**
31
+ * Port of a local `respan collector`. When set, `base_url` points at it
32
+ * (http://127.0.0.1:<port>) instead of `baseUrl`, so traces are delivered
33
+ * through the collector's persistent queue.
34
+ */
35
+ collectorPort?: number;
36
+ /** Custom attributes, stored under `metadata` and merged with existing ones. */
37
+ attrs?: Record<string, string>;
38
+ }
39
+ /**
40
+ * Build the respan.json contents for the @respan/instrumentation-pi extension.
41
+ *
42
+ * Keys are snake_case and match the extension's config loader:
43
+ * `enabled`, `customer_id`, `workflow_name`, `project_id`, `trace_scope`,
44
+ * `base_url`, `metadata`.
45
+ * Nothing secret goes here — the API key is resolved at runtime from
46
+ * RESPAN_API_KEY or ~/.respan/credentials.json. Existing keys are preserved so
47
+ * re-running the command only layers the flags that were passed.
48
+ */
49
+ export declare function buildPiRespanConfig(existing: Record<string, unknown>, opts: PiRespanConfigOptions): Record<string, unknown>;
@@ -0,0 +1,74 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ import { DEFAULT_BASE_URL } from './integrate.js';
4
+ import { collectorBaseUrl } from './collector.js';
5
+ // Pure helpers for `respan integrate pi`. Kept free of oclif / process state so
6
+ // they can be unit-tested directly (tests/integrate-pi.test.mjs).
7
+ /** npm package that ships the Respan pi extension. */
8
+ export const PI_PACKAGE_NAME = '@respan/instrumentation-pi';
9
+ /** Package spec understood by `pi install` / `pi remove`. */
10
+ export const PI_PACKAGE_SPEC = `npm:${PI_PACKAGE_NAME}`;
11
+ /**
12
+ * Arguments for the `pi` binary that install the extension package.
13
+ *
14
+ * - global (default): `pi install npm:@respan/instrumentation-pi`
15
+ * - local: `pi install -l npm:@respan/instrumentation-pi`
16
+ */
17
+ export function piInstallArgs(scope) {
18
+ return scope === 'local'
19
+ ? ['install', '-l', PI_PACKAGE_SPEC]
20
+ : ['install', PI_PACKAGE_SPEC];
21
+ }
22
+ /**
23
+ * Location of the non-secret config file read by the extension:
24
+ * `~/.pi/agent/respan.json` (global) or `<projectRoot>/.pi/respan.json` (local).
25
+ * The extension applies defaults → global → project → env, so both may coexist.
26
+ */
27
+ export function piConfigPath(scope, projectRoot, home = os.homedir()) {
28
+ return scope === 'local'
29
+ ? path.join(projectRoot, '.pi', 'respan.json')
30
+ : path.join(home, '.pi', 'agent', 'respan.json');
31
+ }
32
+ function isPlainObject(value) {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
34
+ }
35
+ /**
36
+ * Build the respan.json contents for the @respan/instrumentation-pi extension.
37
+ *
38
+ * Keys are snake_case and match the extension's config loader:
39
+ * `enabled`, `customer_id`, `workflow_name`, `project_id`, `trace_scope`,
40
+ * `base_url`, `metadata`.
41
+ * Nothing secret goes here — the API key is resolved at runtime from
42
+ * RESPAN_API_KEY or ~/.respan/credentials.json. Existing keys are preserved so
43
+ * re-running the command only layers the flags that were passed.
44
+ */
45
+ export function buildPiRespanConfig(existing, opts) {
46
+ const config = { ...existing, enabled: opts.enabled };
47
+ if (opts.customerId) {
48
+ config.customer_id = opts.customerId;
49
+ }
50
+ if (opts.workflowName) {
51
+ config.workflow_name = opts.workflowName;
52
+ }
53
+ if (opts.projectId) {
54
+ config.project_id = opts.projectId;
55
+ }
56
+ if (opts.traceScope) {
57
+ config.trace_scope = opts.traceScope;
58
+ }
59
+ if (opts.collectorPort !== undefined) {
60
+ config.base_url = collectorBaseUrl(opts.collectorPort);
61
+ }
62
+ else if (opts.baseUrl) {
63
+ const normalized = opts.baseUrl.replace(/\/+$/, '');
64
+ if (normalized && normalized !== DEFAULT_BASE_URL) {
65
+ config.base_url = normalized;
66
+ }
67
+ }
68
+ const attrs = opts.attrs ?? {};
69
+ if (Object.keys(attrs).length > 0) {
70
+ const existingMetadata = isPlainObject(existing.metadata) ? existing.metadata : {};
71
+ config.metadata = { ...existingMetadata, ...attrs };
72
+ }
73
+ return config;
74
+ }