@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.
- package/bin/dev.js +0 -0
- package/dist/commands/collector/config.d.ts +21 -0
- package/dist/commands/collector/config.js +73 -0
- package/dist/commands/collector/start.d.ts +26 -0
- package/dist/commands/collector/start.js +198 -0
- package/dist/commands/collector/status.d.ts +17 -0
- package/dist/commands/collector/status.js +113 -0
- package/dist/commands/collector/stop.d.ts +18 -0
- package/dist/commands/collector/stop.js +91 -0
- package/dist/commands/integrate/pi.d.ts +28 -0
- package/dist/commands/integrate/pi.js +198 -0
- package/dist/lib/collector-runtime.d.ts +93 -0
- package/dist/lib/collector-runtime.js +361 -0
- package/dist/lib/collector.d.ts +135 -0
- package/dist/lib/collector.js +289 -0
- package/dist/lib/pi-integrate.d.ts +49 -0
- package/dist/lib/pi-integrate.js +74 -0
- package/dist/lib/skill-refs.generated.d.ts +1 -1
- package/dist/lib/skill-refs.generated.js +59 -14
- package/oclif.manifest.json +1309 -687
- package/package.json +8 -3
|
@@ -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
|
+
}
|