@respan/cli 0.13.3 → 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,21 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class CollectorConfig extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ print: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ 'export-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'data-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'max-size-mib': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'queue-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
+ port: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
17
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ };
20
+ run(): Promise<void>;
21
+ }
@@ -0,0 +1,73 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../lib/base-command.js';
3
+ import { expandHome, writeTextFile } from '../../lib/integrate.js';
4
+ import { COLLECTOR_VERSION, buildCollectorConfig, collectorBaseUrl, collectorPaths, } from '../../lib/collector.js';
5
+ import { collectorSizingFlags, resolveExportUrl } from '../../lib/collector-runtime.js';
6
+ class CollectorConfig extends BaseCommand {
7
+ async run() {
8
+ const { flags } = await this.parse(CollectorConfig);
9
+ this.globalFlags = flags;
10
+ try {
11
+ const paths = collectorPaths();
12
+ const exportUrl = resolveExportUrl(flags);
13
+ const dataDir = expandHome(flags['data-dir'] ?? paths.dataDir);
14
+ const yaml = buildCollectorConfig({
15
+ exportUrl,
16
+ dataDir,
17
+ maxSizeBytes: flags['max-size-mib'] * 1024 * 1024,
18
+ queueSize: flags['queue-size'],
19
+ listen: `127.0.0.1:${flags.port}`,
20
+ });
21
+ if (flags.print) {
22
+ process.stdout.write(yaml);
23
+ return;
24
+ }
25
+ const output = expandHome(flags.output ?? paths.configPath);
26
+ writeTextFile(output, yaml);
27
+ this.log(`Wrote collector config: ${output}`);
28
+ this.log('');
29
+ this.log(`Export URL: ${exportUrl}`);
30
+ this.log(`Queue dir: ${dataDir}`);
31
+ this.log(`Queue limits: ${flags['max-size-mib']} MiB on disk, ${flags['queue-size']} batches`);
32
+ this.log('');
33
+ this.log('Run it:');
34
+ this.log(' respan collector start');
35
+ this.log(` # or: RESPAN_API_KEY=... otelcol-contrib --config ${output}`);
36
+ this.log('');
37
+ this.log('Point the SDK at the collector:');
38
+ this.log(` export RESPAN_BASE_URL=${collectorBaseUrl(flags.port)}`);
39
+ }
40
+ catch (error) {
41
+ this.handleError(error);
42
+ }
43
+ }
44
+ }
45
+ CollectorConfig.description = `Write the Respan collector configuration.
46
+
47
+ The Respan collector is the upstream OpenTelemetry Collector (contrib
48
+ ${COLLECTOR_VERSION}) with a config that receives OTLP/HTTP from the Respan SDKs
49
+ on localhost and forwards it to Respan through a bounded, persistent
50
+ (write-ahead-log) queue — spans survive Respan outages and collector
51
+ restarts without unbounded local growth.
52
+
53
+ The written file never contains your API key: it reads RESPAN_API_KEY from
54
+ the environment. Use "respan collector start" to run it, or run
55
+ otelcol-contrib yourself with --config <file>.`;
56
+ CollectorConfig.examples = [
57
+ 'respan collector config',
58
+ 'respan collector config --print',
59
+ 'respan collector config --output ./collector.yaml --max-size-mib 1024',
60
+ 'respan collector config --export-url https://respan.internal/api/v2/traces',
61
+ ];
62
+ CollectorConfig.flags = {
63
+ ...BaseCommand.baseFlags,
64
+ ...collectorSizingFlags,
65
+ output: Flags.string({
66
+ description: 'Where to write the config (default: ~/.respan/collector/config.yaml)',
67
+ }),
68
+ print: Flags.boolean({
69
+ description: 'Print the config to stdout instead of writing it',
70
+ default: false,
71
+ }),
72
+ };
73
+ export default CollectorConfig;
@@ -0,0 +1,26 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class CollectorStart extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ 'export-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'data-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'max-size-mib': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'queue-size': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
11
+ port: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
+ docker: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ binary: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
18
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
19
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ };
21
+ run(): Promise<void>;
22
+ /** Poll the health endpoint behind a spinner that fails visibly on timeout / exit. */
23
+ private awaitHealth;
24
+ private printStarted;
25
+ private resolveApiKey;
26
+ }
@@ -0,0 +1,198 @@
1
+ import * as fs from 'node:fs';
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../lib/base-command.js';
4
+ import { ensureDir, expandHome, writeTextFile } from '../../lib/integrate.js';
5
+ import { COLLECTOR_IMAGE, CONTAINER_NAME, DEFAULT_HEALTH_PORT, VOLUME_NAME, binaryDownloadUrl, buildCollectorConfig, collectorBaseUrl, collectorPaths, dockerRunArgs, dockerVolumeInitArgs, } from '../../lib/collector.js';
6
+ import { checkHealth, collectorModeFlags, collectorSizingFlags, detectRunning, dockerAvailable, dockerVolumeExists, ensureCollectorBinary, healthUrl, isPidAlive, metricsUrl, resolveExportUrl, runDocker, spawnCollectorBinary, tailFile, waitForHealth, } from '../../lib/collector-runtime.js';
7
+ const HEALTH_TIMEOUT_MS = 20000;
8
+ class CollectorStart extends BaseCommand {
9
+ async run() {
10
+ const { flags } = await this.parse(CollectorStart);
11
+ this.globalFlags = flags;
12
+ try {
13
+ const dryRun = flags['dry-run'];
14
+ const apiKey = this.resolveApiKey();
15
+ const paths = collectorPaths();
16
+ let mode;
17
+ if (flags.docker) {
18
+ if (!dockerAvailable()) {
19
+ this.error('Docker is not available (`docker info` failed). Start Docker or use --binary.');
20
+ }
21
+ mode = 'docker';
22
+ }
23
+ else if (flags.binary) {
24
+ mode = 'binary';
25
+ }
26
+ else {
27
+ mode = dockerAvailable() ? 'docker' : 'binary';
28
+ }
29
+ // ── Refuse to double-start ────────────────────────────────────
30
+ const running = detectRunning(paths);
31
+ if (running?.mode === 'docker') {
32
+ const status = running.container?.status ?? 'unknown';
33
+ this.error(`Container ${CONTAINER_NAME} already exists (${status}). ` +
34
+ 'Run `respan collector stop` first' +
35
+ (status === 'running' ? '.' : `, or resume it with \`docker start ${CONTAINER_NAME}\`.`));
36
+ }
37
+ if (running?.mode === 'binary') {
38
+ this.error(`The collector is already running (pid ${running.pid}). ` +
39
+ 'See `respan collector status` or stop it with `respan collector stop`.');
40
+ }
41
+ if (!dryRun && (await checkHealth()).healthy) {
42
+ this.error(`Something already answers on ${healthUrl()} (another collector?) but was not started by this CLI. ` +
43
+ 'Stop it before starting a new one.');
44
+ }
45
+ const exportUrl = resolveExportUrl(flags);
46
+ const port = flags.port;
47
+ const maxSizeBytes = flags['max-size-mib'] * 1024 * 1024;
48
+ const queueSize = flags['queue-size'];
49
+ const dataDir = expandHome(flags['data-dir'] ?? paths.dataDir);
50
+ const env = { ...process.env, RESPAN_API_KEY: apiKey };
51
+ if (mode === 'docker') {
52
+ // Everything is passed as container env; the mounted file is the pure template.
53
+ const yaml = buildCollectorConfig();
54
+ const runArgs = dockerRunArgs({ configPath: paths.configPath, port, exportUrl, maxSizeBytes, queueSize });
55
+ const needsVolume = !dockerVolumeExists();
56
+ if (flags['data-dir']) {
57
+ this.warn(`--data-dir is ignored in docker mode; the queue lives in the ${VOLUME_NAME} volume.`);
58
+ }
59
+ if (dryRun) {
60
+ this.log('[dry-run] Mode: docker');
61
+ this.log(`[dry-run] Would write: ${paths.configPath}`);
62
+ if (needsVolume) {
63
+ this.log(`[dry-run] Would run: docker ${dockerVolumeInitArgs().join(' ')}`);
64
+ }
65
+ this.log(`[dry-run] Would run: RESPAN_API_KEY=*** docker ${runArgs.join(' ')}`);
66
+ this.log(`[dry-run] Would wait for ${healthUrl()}`);
67
+ return;
68
+ }
69
+ writeTextFile(paths.configPath, yaml);
70
+ this.log(`Wrote collector config: ${paths.configPath}`);
71
+ if (needsVolume) {
72
+ const init = await this.spin(`Creating volume ${VOLUME_NAME}`, () => runDocker(dockerVolumeInitArgs()));
73
+ if (init.status !== 0) {
74
+ this.error(`Failed to prepare volume ${VOLUME_NAME}: ${init.stderr.trim()}`);
75
+ }
76
+ }
77
+ const started = await this.spin(`Starting ${CONTAINER_NAME} (${COLLECTOR_IMAGE})`, () => runDocker(runArgs, env));
78
+ if (started.status !== 0) {
79
+ this.error(`docker run failed: ${started.stderr.trim()}`);
80
+ }
81
+ const health = await this.awaitHealth();
82
+ if (!health.healthy) {
83
+ this.warn(`Collector is not healthy yet (${health.error ?? health.status}). ` +
84
+ `Check \`docker logs ${CONTAINER_NAME}\` or \`respan collector status\`.`);
85
+ }
86
+ this.printStarted(mode, health, port, exportUrl);
87
+ return;
88
+ }
89
+ // ── Binary mode ───────────────────────────────────────────────
90
+ const yaml = buildCollectorConfig({ exportUrl, dataDir, maxSizeBytes, queueSize, listen: `127.0.0.1:${port}` });
91
+ if (dryRun) {
92
+ this.log('[dry-run] Mode: binary');
93
+ if (!fs.existsSync(paths.binaryPath)) {
94
+ this.log(`[dry-run] Would download ${binaryDownloadUrl()} to ${paths.binDir}`);
95
+ }
96
+ this.log(`[dry-run] Would write: ${paths.configPath} (preview: respan collector config --print)`);
97
+ this.log(`[dry-run] Would run: RESPAN_API_KEY=*** ${paths.binaryPath} --config ${paths.configPath}`);
98
+ this.log(`[dry-run] detached; log: ${paths.logPath}; pid file: ${paths.pidPath}`);
99
+ this.log(`[dry-run] Would wait for ${healthUrl()}`);
100
+ return;
101
+ }
102
+ await ensureCollectorBinary(paths, this);
103
+ writeTextFile(paths.configPath, yaml);
104
+ this.log(`Wrote collector config: ${paths.configPath}`);
105
+ ensureDir(dataDir);
106
+ const pid = spawnCollectorBinary(paths, env);
107
+ this.log(`Started otelcol-contrib (pid ${pid}); log: ${paths.logPath}`);
108
+ const health = await this.awaitHealth(() => isPidAlive(pid));
109
+ if (!health.healthy) {
110
+ if (!isPidAlive(pid)) {
111
+ fs.rmSync(paths.pidPath, { force: true });
112
+ this.error(`The collector exited during startup. Last lines of ${paths.logPath}:\n${tailFile(paths.logPath, 15)}`);
113
+ }
114
+ this.warn(`Collector is not healthy yet (${health.error ?? health.status}). ` +
115
+ `Check ${paths.logPath} or \`respan collector status\`.`);
116
+ }
117
+ this.printStarted(mode, health, port, exportUrl);
118
+ }
119
+ catch (error) {
120
+ this.handleError(error);
121
+ }
122
+ }
123
+ /** Poll the health endpoint behind a spinner that fails visibly on timeout / exit. */
124
+ async awaitHealth(isAlive) {
125
+ let unhealthy;
126
+ try {
127
+ return await this.spin('Waiting for the collector to become healthy', async () => {
128
+ const health = await waitForHealth(DEFAULT_HEALTH_PORT, HEALTH_TIMEOUT_MS, isAlive);
129
+ if (!health.healthy) {
130
+ unhealthy = health;
131
+ throw new Error(health.error ?? health.status ?? 'unhealthy');
132
+ }
133
+ return health;
134
+ });
135
+ }
136
+ catch (error) {
137
+ if (unhealthy)
138
+ return unhealthy;
139
+ throw error;
140
+ }
141
+ }
142
+ printStarted(mode, health, port, exportUrl) {
143
+ this.log('');
144
+ this.log(`Collector ${health.healthy ? 'is healthy' : 'started'} (${mode}).`);
145
+ this.log(` Receiver: ${collectorBaseUrl(port)}/api/v2/traces`);
146
+ this.log(` Health: ${healthUrl()}`);
147
+ this.log(` Metrics: ${metricsUrl()}`);
148
+ this.log(` Export: ${exportUrl}`);
149
+ this.log('');
150
+ this.log('Point the SDK at the collector:');
151
+ this.log(` export RESPAN_BASE_URL=${collectorBaseUrl(port)}`);
152
+ this.log('');
153
+ this.log('Check / stop:');
154
+ this.log(' respan collector status');
155
+ this.log(' respan collector stop');
156
+ }
157
+ resolveApiKey() {
158
+ const auth = this.getAuth();
159
+ if (auth.apiKey)
160
+ return auth.apiKey;
161
+ if (auth.accessToken) {
162
+ this.warn('Using access token (JWT) which may expire. Consider using an API key instead.');
163
+ return auth.accessToken;
164
+ }
165
+ this.error('No API key found. Pass --api-key, set RESPAN_API_KEY, or run: respan auth login');
166
+ }
167
+ }
168
+ CollectorStart.description = `Start a local Respan collector.
169
+
170
+ Runs the OpenTelemetry Collector (contrib) with the Respan config: the SDKs
171
+ send to http://127.0.0.1:4318, and the collector forwards to Respan through
172
+ a bounded, persistent queue so traces survive outages and restarts.
173
+
174
+ Modes:
175
+ --docker docker run otel/opentelemetry-collector-contrib with the queue in
176
+ the ${VOLUME_NAME} volume (default when Docker is available)
177
+ --binary download otelcol-contrib to ~/.respan/collector/bin and run it
178
+ detached, logging to ~/.respan/collector/collector.log
179
+
180
+ The API key is passed to the collector through its environment only; the
181
+ config file never contains it.`;
182
+ CollectorStart.examples = [
183
+ 'respan collector start',
184
+ 'respan collector start --binary',
185
+ 'respan collector start --docker --max-size-mib 1024 --queue-size 20000',
186
+ 'respan collector start --export-url https://respan.internal/api/v2/traces',
187
+ 'respan collector start --dry-run',
188
+ ];
189
+ CollectorStart.flags = {
190
+ ...BaseCommand.baseFlags,
191
+ ...collectorModeFlags,
192
+ ...collectorSizingFlags,
193
+ 'dry-run': Flags.boolean({
194
+ description: 'Show what would be done without starting anything',
195
+ default: false,
196
+ }),
197
+ };
198
+ export default CollectorStart;
@@ -0,0 +1,17 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class CollectorStatus extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ port: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
7
+ docker: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ binary: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,113 @@
1
+ import * as fs from 'node:fs';
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../lib/base-command.js';
4
+ import { GREEN, RED, YELLOW, DIM, RESET } from '../../lib/colors.js';
5
+ import { CONTAINER_NAME, DEFAULT_COLLECTOR_PORT, VOLUME_NAME, collectorBaseUrl, collectorPaths, formatBytes, } from '../../lib/collector.js';
6
+ import { checkHealth, collectorModeFlags, detectRunning, directorySize, fetchMetrics, healthUrl, metricsUrl, } from '../../lib/collector-runtime.js';
7
+ class CollectorStatus extends BaseCommand {
8
+ async run() {
9
+ const { flags } = await this.parse(CollectorStatus);
10
+ this.globalFlags = flags;
11
+ try {
12
+ const paths = collectorPaths();
13
+ const mode = flags.docker ? 'docker' : flags.binary ? 'binary' : undefined;
14
+ const running = detectRunning(paths, mode);
15
+ const health = await checkHealth();
16
+ const metrics = await fetchMetrics();
17
+ const dataDirBytes = fs.existsSync(paths.dataDir) ? directorySize(paths.dataDir) : undefined;
18
+ const isRunning = running?.mode === 'docker' ? Boolean(running.container?.running) : Boolean(running);
19
+ const result = {
20
+ mode: running?.mode ?? null,
21
+ running: isRunning,
22
+ healthy: health.healthy,
23
+ health,
24
+ container: running?.container ?? null,
25
+ pid: running?.pid ?? null,
26
+ metrics: metrics ?? null,
27
+ dataDir: running?.mode === 'docker' ? null : paths.dataDir,
28
+ dataDirBytes: running?.mode === 'docker' ? null : dataDirBytes ?? null,
29
+ volume: running?.mode === 'docker' ? VOLUME_NAME : null,
30
+ configPath: paths.configPath,
31
+ logPath: running?.mode === 'binary' ? paths.logPath : null,
32
+ baseUrl: collectorBaseUrl(flags.port),
33
+ };
34
+ if (flags.json) {
35
+ this.outputResult(result);
36
+ return;
37
+ }
38
+ const ok = `${GREEN}✓${RESET}`;
39
+ const bad = `${RED}✗${RESET}`;
40
+ const warn = `${YELLOW}!${RESET}`;
41
+ this.log('');
42
+ if (running?.mode === 'docker' && running.container) {
43
+ const c = running.container;
44
+ const mark = c.running ? ok : bad;
45
+ this.log(` ${mark} Mode: docker (container ${CONTAINER_NAME}: ${c.status}${c.restartCount ? `, ${c.restartCount} restarts` : ''})`);
46
+ if (c.running && c.startedAt)
47
+ this.log(` Started: ${c.startedAt}`);
48
+ if (!c.running)
49
+ this.log(` ${DIM}Resume with: docker start ${CONTAINER_NAME} (or respan collector stop && respan collector start)${RESET}`);
50
+ }
51
+ else if (running?.mode === 'binary') {
52
+ this.log(` ${ok} Mode: binary (pid ${running.pid})`);
53
+ this.log(` Log: ${paths.logPath}`);
54
+ }
55
+ else {
56
+ this.log(` ${bad} Mode: not running`);
57
+ this.log(` ${DIM}Start with: respan collector start${RESET}`);
58
+ }
59
+ if (health.healthy) {
60
+ this.log(` ${ok} Health: ${health.status}${health.uptime ? ` (up ${health.uptime})` : ''} ${DIM}${healthUrl()}${RESET}`);
61
+ }
62
+ else {
63
+ this.log(` ${bad} Health: ${health.error ?? health.status ?? 'unreachable'} ${DIM}${healthUrl()}${RESET}`);
64
+ }
65
+ if (metrics) {
66
+ const queueMark = metrics.queueSize > 0 ? warn : ok;
67
+ this.log(` ${queueMark} Queue: ${metrics.queueSize} / ${metrics.queueCapacity} batches waiting ${DIM}${metricsUrl()}${RESET}`);
68
+ this.log(` Spans: sent ${metrics.sentSpans}, enqueue-failed ${metrics.enqueueFailedSpans}, send-failed ${metrics.sendFailedSpans}`);
69
+ if (metrics.enqueueFailedSpans > 0) {
70
+ this.log(` ${warn} ${metrics.enqueueFailedSpans} spans were dropped because the queue was full (raise --max-size-mib / --queue-size).`);
71
+ }
72
+ if (metrics.sendFailedSpans > 0) {
73
+ this.log(` ${warn} ${metrics.sendFailedSpans} spans were rejected by Respan (check the API key and export URL).`);
74
+ }
75
+ }
76
+ else {
77
+ this.log(` ${DIM} Metrics: unavailable (${metricsUrl()})${RESET}`);
78
+ }
79
+ if (running?.mode === 'docker') {
80
+ this.log(` Queue: docker volume ${VOLUME_NAME}`);
81
+ }
82
+ else if (dataDirBytes !== undefined) {
83
+ this.log(` Queue: ${paths.dataDir} (${formatBytes(dataDirBytes)})`);
84
+ }
85
+ this.log(` Config: ${paths.configPath}${fs.existsSync(paths.configPath) ? '' : ` ${DIM}(not written yet)${RESET}`}`);
86
+ this.log('');
87
+ this.log(` SDK: export RESPAN_BASE_URL=${collectorBaseUrl(flags.port)}`);
88
+ this.log('');
89
+ }
90
+ catch (error) {
91
+ this.handleError(error);
92
+ }
93
+ }
94
+ }
95
+ CollectorStatus.description = `Show the local Respan collector's health and queue.
96
+
97
+ Reports how the collector is running (Docker container or binary), the
98
+ health_check result, and the exporter metrics that matter for delivery:
99
+ queued batches, spans sent, spans dropped because the queue was full
100
+ (enqueue-failed) and spans rejected by Respan (send-failed).`;
101
+ CollectorStatus.examples = [
102
+ 'respan collector status',
103
+ 'respan collector status --json',
104
+ ];
105
+ CollectorStatus.flags = {
106
+ ...BaseCommand.baseFlags,
107
+ ...collectorModeFlags,
108
+ port: Flags.integer({
109
+ description: 'Local OTLP/HTTP port used in the SDK hint',
110
+ default: DEFAULT_COLLECTOR_PORT,
111
+ }),
112
+ };
113
+ export default CollectorStatus;
@@ -0,0 +1,18 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class CollectorStop extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ purge: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ docker: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ binary: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ }
@@ -0,0 +1,91 @@
1
+ import * as fs from 'node:fs';
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../lib/base-command.js';
4
+ import { CONTAINER_NAME, VOLUME_NAME, collectorPaths } from '../../lib/collector.js';
5
+ import { collectorModeFlags, detectRunning, dockerVolumeExists, runDocker, stopPid, } from '../../lib/collector-runtime.js';
6
+ class CollectorStop extends BaseCommand {
7
+ async run() {
8
+ const { flags } = await this.parse(CollectorStop);
9
+ this.globalFlags = flags;
10
+ try {
11
+ if (flags.purge && !flags.yes) {
12
+ this.error('--purge deletes any spans still queued for delivery. Re-run with --yes to confirm.');
13
+ }
14
+ const paths = collectorPaths();
15
+ const mode = flags.docker ? 'docker' : flags.binary ? 'binary' : undefined;
16
+ const running = detectRunning(paths, mode);
17
+ let stopped = false;
18
+ if (running?.mode === 'docker') {
19
+ const result = await runDocker(['rm', '-f', CONTAINER_NAME]);
20
+ if (result.status !== 0) {
21
+ this.error(`Failed to remove container ${CONTAINER_NAME}: ${result.stderr.trim()}`);
22
+ }
23
+ this.log(`Removed container ${CONTAINER_NAME}.`);
24
+ stopped = true;
25
+ }
26
+ else if (running?.mode === 'binary' && running.pid !== undefined) {
27
+ const outcome = await stopPid(running.pid);
28
+ fs.rmSync(paths.pidPath, { force: true });
29
+ if (outcome === 'killed') {
30
+ this.log(`Collector (pid ${running.pid}) did not exit after SIGTERM; killed it.`);
31
+ }
32
+ else {
33
+ this.log(`Stopped collector (pid ${running.pid}).`);
34
+ }
35
+ stopped = true;
36
+ }
37
+ else {
38
+ this.log('The collector is not running.');
39
+ }
40
+ if (flags.purge) {
41
+ let purged = false;
42
+ if (mode !== 'binary' && dockerVolumeExists()) {
43
+ const result = await runDocker(['volume', 'rm', VOLUME_NAME]);
44
+ if (result.status === 0) {
45
+ this.log(`Removed volume ${VOLUME_NAME}.`);
46
+ purged = true;
47
+ }
48
+ else {
49
+ this.warn(`Failed to remove volume ${VOLUME_NAME}: ${result.stderr.trim()}`);
50
+ }
51
+ }
52
+ if (mode !== 'docker' && fs.existsSync(paths.dataDir)) {
53
+ fs.rmSync(paths.dataDir, { recursive: true, force: true });
54
+ this.log(`Removed queue directory ${paths.dataDir}.`);
55
+ purged = true;
56
+ }
57
+ if (!purged)
58
+ this.log('No queued data to remove.');
59
+ }
60
+ else if (stopped) {
61
+ this.log('Queued spans are kept on disk and will be delivered when the collector starts again.');
62
+ this.log('To delete them: respan collector stop --purge --yes');
63
+ }
64
+ }
65
+ catch (error) {
66
+ this.handleError(error);
67
+ }
68
+ }
69
+ }
70
+ CollectorStop.description = `Stop the local Respan collector.
71
+
72
+ Removes the ${CONTAINER_NAME} container or stops the detached binary. Spans
73
+ still queued on disk are kept and delivered the next time the collector
74
+ starts; pass --purge --yes to delete them.`;
75
+ CollectorStop.examples = [
76
+ 'respan collector stop',
77
+ 'respan collector stop --purge --yes',
78
+ ];
79
+ CollectorStop.flags = {
80
+ ...BaseCommand.baseFlags,
81
+ ...collectorModeFlags,
82
+ purge: Flags.boolean({
83
+ description: `Also delete the queued data (the ${VOLUME_NAME} volume / data dir). Requires --yes`,
84
+ default: false,
85
+ }),
86
+ yes: Flags.boolean({
87
+ description: 'Confirm deleting queued spans with --purge',
88
+ default: false,
89
+ }),
90
+ };
91
+ export default CollectorStop;
@@ -0,0 +1,28 @@
1
+ import { BaseCommand } from '../../lib/base-command.js';
2
+ export default class IntegratePi extends BaseCommand {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ 'workflow-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'span-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ 'trace-scope': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'with-collector': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'collector-port': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ enable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ disable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ 'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ 'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
+ attrs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
18
+ 'customer-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
+ 'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
23
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
24
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
25
+ };
26
+ run(): Promise<void>;
27
+ private resolveApiKey;
28
+ }