@omen.foundation/node-microservice-runtime 0.1.18 → 0.1.19
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/dist/collector-manager.cjs +215 -0
- package/dist/collector-manager.d.ts +26 -0
- package/dist/collector-manager.d.ts.map +1 -0
- package/dist/collector-manager.js +244 -0
- package/dist/collector-manager.js.map +1 -0
- package/dist/logger.cjs +48 -15
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +55 -19
- package/dist/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/collector-manager.ts +299 -0
- package/src/logger.ts +66 -25
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isCollectorRunning = isCollectorRunning;
|
|
7
|
+
exports.startCollector = startCollector;
|
|
8
|
+
exports.discoverOrStartCollector = discoverOrStartCollector;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const fs_1 = require("fs");
|
|
11
|
+
const path_1 = require("path");
|
|
12
|
+
const promises_1 = require("stream/promises");
|
|
13
|
+
const zlib_1 = require("zlib");
|
|
14
|
+
const dgram_1 = __importDefault(require("dgram"));
|
|
15
|
+
const COLLECTOR_VERSION = '0.0.123';
|
|
16
|
+
const COLLECTOR_DOWNLOAD_BASE = `https://collectors.beamable.com/version/${COLLECTOR_VERSION}`;
|
|
17
|
+
const DISCOVERY_PORT = parseInt(process.env.BEAM_COLLECTOR_DISCOVERY_PORT || '8688', 10);
|
|
18
|
+
const DISCOVERY_DELAY = 100;
|
|
19
|
+
const DISCOVERY_ATTEMPTS = 10;
|
|
20
|
+
function getCollectorStoragePath() {
|
|
21
|
+
const tempDir = process.env.TMPDIR || process.env.TMP || '/tmp';
|
|
22
|
+
return (0, path_1.join)(tempDir, 'beam', 'collectors', COLLECTOR_VERSION);
|
|
23
|
+
}
|
|
24
|
+
function getCollectorBinaryName() {
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
const arch = process.arch;
|
|
27
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
28
|
+
return 'beamable-collector-linux-amd64';
|
|
29
|
+
}
|
|
30
|
+
else if (platform === 'linux' && arch === 'arm64') {
|
|
31
|
+
return 'beamable-collector-linux-arm64';
|
|
32
|
+
}
|
|
33
|
+
else if (platform === 'darwin' && arch === 'x64') {
|
|
34
|
+
return 'beamable-collector-darwin-amd64';
|
|
35
|
+
}
|
|
36
|
+
else if (platform === 'darwin' && arch === 'arm64') {
|
|
37
|
+
return 'beamable-collector-darwin-arm64';
|
|
38
|
+
}
|
|
39
|
+
else if (platform === 'win32' && arch === 'x64') {
|
|
40
|
+
return 'beamable-collector-windows-amd64.exe';
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
43
|
+
}
|
|
44
|
+
async function downloadAndDecompressGzip(url, outputPath, makeExecutable = false) {
|
|
45
|
+
const response = await fetch(url);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
const dir = require('path').dirname(outputPath);
|
|
50
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
51
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
const gunzip = (0, zlib_1.createGunzip)();
|
|
54
|
+
const writeStream = (0, fs_1.createWriteStream)(outputPath);
|
|
55
|
+
await (0, promises_1.pipeline)(response.body, gunzip, writeStream);
|
|
56
|
+
if (makeExecutable && process.platform !== 'win32') {
|
|
57
|
+
try {
|
|
58
|
+
(0, fs_1.chmodSync)(outputPath, 0o755);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error(`Failed to make ${outputPath} executable:`, error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function resolveCollector(allowDownload = true) {
|
|
66
|
+
const basePath = getCollectorStoragePath();
|
|
67
|
+
const binaryName = getCollectorBinaryName();
|
|
68
|
+
const configName = 'clickhouse-config.yaml';
|
|
69
|
+
const binaryPath = (0, path_1.join)(basePath, binaryName);
|
|
70
|
+
const configPath = (0, path_1.join)(basePath, configName);
|
|
71
|
+
const itemsToDownload = [];
|
|
72
|
+
if (!(0, fs_1.existsSync)(binaryPath) && allowDownload) {
|
|
73
|
+
const binaryUrl = `${COLLECTOR_DOWNLOAD_BASE}/${binaryName}.gz`;
|
|
74
|
+
itemsToDownload.push({ url: binaryUrl, path: binaryPath, executable: true });
|
|
75
|
+
}
|
|
76
|
+
if (!(0, fs_1.existsSync)(configPath) && allowDownload) {
|
|
77
|
+
const configUrl = `${COLLECTOR_DOWNLOAD_BASE}/${configName}.gz`;
|
|
78
|
+
itemsToDownload.push({ url: configUrl, path: configPath, executable: false });
|
|
79
|
+
}
|
|
80
|
+
for (const item of itemsToDownload) {
|
|
81
|
+
await downloadAndDecompressGzip(item.url, item.path, item.executable);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
binaryPath: (0, fs_1.existsSync)(binaryPath) ? binaryPath : null,
|
|
85
|
+
configPath: (0, fs_1.existsSync)(configPath) ? configPath : null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function discoverCollectorViaUDP(timeoutMs = 5000) {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const socket = dgram_1.default.createSocket('udp4');
|
|
91
|
+
const discovered = [];
|
|
92
|
+
let timeout;
|
|
93
|
+
socket.on('message', (msg) => {
|
|
94
|
+
try {
|
|
95
|
+
const message = JSON.parse(msg.toString());
|
|
96
|
+
if (message.version === COLLECTOR_VERSION && message.status === 'READY') {
|
|
97
|
+
discovered.push(message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
socket.on('error', () => {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
socket.close();
|
|
106
|
+
resolve(null);
|
|
107
|
+
});
|
|
108
|
+
socket.bind(() => {
|
|
109
|
+
socket.setBroadcast(true);
|
|
110
|
+
timeout = setTimeout(() => {
|
|
111
|
+
socket.close();
|
|
112
|
+
resolve(discovered.length > 0 ? discovered[0] : null);
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function isCollectorRunning() {
|
|
118
|
+
try {
|
|
119
|
+
const discovered = await discoverCollectorViaUDP(2000);
|
|
120
|
+
if (discovered) {
|
|
121
|
+
return {
|
|
122
|
+
isRunning: true,
|
|
123
|
+
isReady: discovered.status === 'READY',
|
|
124
|
+
pid: discovered.pid,
|
|
125
|
+
otlpEndpoint: discovered.otlpEndpoint,
|
|
126
|
+
version: discovered.version,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
isRunning: false,
|
|
134
|
+
isReady: false,
|
|
135
|
+
pid: 0,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function startCollector(logger, otlpEndpoint) {
|
|
139
|
+
var _a, _b;
|
|
140
|
+
const collectorInfo = await resolveCollector(true);
|
|
141
|
+
if (!collectorInfo.binaryPath) {
|
|
142
|
+
throw new Error('Collector binary not found and download failed');
|
|
143
|
+
}
|
|
144
|
+
if (!collectorInfo.configPath) {
|
|
145
|
+
throw new Error('Collector config not found and download failed');
|
|
146
|
+
}
|
|
147
|
+
let localEndpoint = otlpEndpoint || 'localhost:4318';
|
|
148
|
+
localEndpoint = localEndpoint.replace(/^http:\/\//, '').replace(/^https:\/\//, '');
|
|
149
|
+
const env = {
|
|
150
|
+
...process.env,
|
|
151
|
+
BEAM_OTLP_HTTP_ENDPOINT: localEndpoint,
|
|
152
|
+
BEAM_CLICKHOUSE_ENDPOINT: process.env.BEAM_CLICKHOUSE_ENDPOINT || '',
|
|
153
|
+
BEAM_CLICKHOUSE_USERNAME: process.env.BEAM_CLICKHOUSE_USERNAME || '',
|
|
154
|
+
BEAM_CLICKHOUSE_PASSWORD: process.env.BEAM_CLICKHOUSE_PASSWORD || '',
|
|
155
|
+
BEAM_COLLECTOR_DISCOVERY_PORT: String(DISCOVERY_PORT),
|
|
156
|
+
BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT: process.env.BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT || '5s',
|
|
157
|
+
BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE: process.env.BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE || '5000',
|
|
158
|
+
BEAM_CLICKHOUSE_EXPORTER_TIMEOUT: process.env.BEAM_CLICKHOUSE_EXPORTER_TIMEOUT || '5s',
|
|
159
|
+
BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE: process.env.BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE || '1000',
|
|
160
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED || 'true',
|
|
161
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL || '5s',
|
|
162
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL || '30s',
|
|
163
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME || '300s',
|
|
164
|
+
};
|
|
165
|
+
const collectorProcess = (0, child_process_1.spawn)(collectorInfo.binaryPath, ['--config', collectorInfo.configPath], {
|
|
166
|
+
env,
|
|
167
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
|
+
detached: false,
|
|
169
|
+
});
|
|
170
|
+
(_a = collectorProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
171
|
+
logger.debug(`[Collector] ${data.toString().trim()}`);
|
|
172
|
+
});
|
|
173
|
+
(_b = collectorProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
174
|
+
logger.debug(`[Collector ERR] ${data.toString().trim()}`);
|
|
175
|
+
});
|
|
176
|
+
collectorProcess.on('error', (err) => {
|
|
177
|
+
logger.error(`[Collector] Failed to start: ${err.message}`);
|
|
178
|
+
});
|
|
179
|
+
collectorProcess.on('exit', (code) => {
|
|
180
|
+
logger.warn(`[Collector] Process exited with code ${code}`);
|
|
181
|
+
});
|
|
182
|
+
logger.info(`[Collector] Started with PID ${collectorProcess.pid}, endpoint: ${localEndpoint}`);
|
|
183
|
+
return {
|
|
184
|
+
process: collectorProcess,
|
|
185
|
+
endpoint: `http://${localEndpoint}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async function discoverOrStartCollector(logger, standardOtelEnabled) {
|
|
189
|
+
if (!standardOtelEnabled) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const status = await isCollectorRunning();
|
|
193
|
+
if (status.isRunning && status.isReady && status.otlpEndpoint) {
|
|
194
|
+
logger.info(`[Collector] Found running collector at ${status.otlpEndpoint}`);
|
|
195
|
+
return `http://${status.otlpEndpoint}`;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
logger.info('[Collector] Starting OpenTelemetry collector...');
|
|
199
|
+
const { endpoint } = await startCollector(logger);
|
|
200
|
+
for (let i = 0; i < DISCOVERY_ATTEMPTS; i++) {
|
|
201
|
+
await new Promise(resolve => setTimeout(resolve, DISCOVERY_DELAY));
|
|
202
|
+
const newStatus = await isCollectorRunning();
|
|
203
|
+
if (newStatus.isRunning && newStatus.isReady) {
|
|
204
|
+
logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint}`);
|
|
205
|
+
return newStatus.otlpEndpoint ? `http://${newStatus.otlpEndpoint}` : endpoint;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
logger.warn('[Collector] Collector started but not yet ready, using configured endpoint');
|
|
209
|
+
return endpoint;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
logger.error(`[Collector] Failed to start collector: ${err instanceof Error ? err.message : String(err)}`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import type { Logger } from 'pino';
|
|
3
|
+
interface CollectorStatus {
|
|
4
|
+
isRunning: boolean;
|
|
5
|
+
isReady: boolean;
|
|
6
|
+
pid: number;
|
|
7
|
+
otlpEndpoint?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks if collector is already running via UDP discovery
|
|
12
|
+
*/
|
|
13
|
+
export declare function isCollectorRunning(): Promise<CollectorStatus>;
|
|
14
|
+
/**
|
|
15
|
+
* Starts the OpenTelemetry collector process
|
|
16
|
+
*/
|
|
17
|
+
export declare function startCollector(logger: Logger, otlpEndpoint?: string): Promise<{
|
|
18
|
+
process: ChildProcess;
|
|
19
|
+
endpoint: string;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Discovers or starts the collector and returns the OTLP endpoint
|
|
23
|
+
*/
|
|
24
|
+
export declare function discoverOrStartCollector(logger: Logger, standardOtelEnabled: boolean): Promise<string | null>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=collector-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector-manager.d.ts","sourceRoot":"","sources":["../src/collector-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,YAAY,EAAE,MAAM,eAAe,CAAC;AAKpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAUnC,UAAU,eAAe;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA2ID;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,eAAe,CAAC,CAqBnE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8DtD;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,mBAAmB,EAAE,OAAO,GAC3B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmCxB"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createWriteStream, existsSync, chmodSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
5
|
+
import { createGunzip } from 'zlib';
|
|
6
|
+
import dgram from 'dgram';
|
|
7
|
+
const COLLECTOR_VERSION = '0.0.123'; // Match C# collector version
|
|
8
|
+
const COLLECTOR_DOWNLOAD_BASE = `https://collectors.beamable.com/version/${COLLECTOR_VERSION}`;
|
|
9
|
+
const DISCOVERY_PORT = parseInt(process.env.BEAM_COLLECTOR_DISCOVERY_PORT || '8688', 10);
|
|
10
|
+
const DISCOVERY_DELAY = 100; // milliseconds
|
|
11
|
+
const DISCOVERY_ATTEMPTS = 10;
|
|
12
|
+
/**
|
|
13
|
+
* Gets the collector storage directory (similar to C# LocalApplicationData/beam/collectors/version)
|
|
14
|
+
*/
|
|
15
|
+
function getCollectorStoragePath() {
|
|
16
|
+
// Use temp directory for now - in containers this should be writable
|
|
17
|
+
const tempDir = process.env.TMPDIR || process.env.TMP || '/tmp';
|
|
18
|
+
return join(tempDir, 'beam', 'collectors', COLLECTOR_VERSION);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Gets the collector binary name for the current platform
|
|
22
|
+
*/
|
|
23
|
+
function getCollectorBinaryName() {
|
|
24
|
+
const platform = process.platform;
|
|
25
|
+
const arch = process.arch;
|
|
26
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
27
|
+
return 'beamable-collector-linux-amd64';
|
|
28
|
+
}
|
|
29
|
+
else if (platform === 'linux' && arch === 'arm64') {
|
|
30
|
+
return 'beamable-collector-linux-arm64';
|
|
31
|
+
}
|
|
32
|
+
else if (platform === 'darwin' && arch === 'x64') {
|
|
33
|
+
return 'beamable-collector-darwin-amd64';
|
|
34
|
+
}
|
|
35
|
+
else if (platform === 'darwin' && arch === 'arm64') {
|
|
36
|
+
return 'beamable-collector-darwin-arm64';
|
|
37
|
+
}
|
|
38
|
+
else if (platform === 'win32' && arch === 'x64') {
|
|
39
|
+
return 'beamable-collector-windows-amd64.exe';
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Downloads and decompresses a gzipped file
|
|
45
|
+
*/
|
|
46
|
+
async function downloadAndDecompressGzip(url, outputPath, makeExecutable = false) {
|
|
47
|
+
const response = await fetch(url);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
const dir = require('path').dirname(outputPath);
|
|
52
|
+
if (!existsSync(dir)) {
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
const gunzip = createGunzip();
|
|
56
|
+
const writeStream = createWriteStream(outputPath);
|
|
57
|
+
await pipeline(response.body, gunzip, writeStream);
|
|
58
|
+
if (makeExecutable && process.platform !== 'win32') {
|
|
59
|
+
try {
|
|
60
|
+
chmodSync(outputPath, 0o755);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error(`Failed to make ${outputPath} executable:`, error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolves the collector binary and config, downloading if needed
|
|
69
|
+
*/
|
|
70
|
+
async function resolveCollector(allowDownload = true) {
|
|
71
|
+
const basePath = getCollectorStoragePath();
|
|
72
|
+
const binaryName = getCollectorBinaryName();
|
|
73
|
+
const configName = 'clickhouse-config.yaml';
|
|
74
|
+
const binaryPath = join(basePath, binaryName);
|
|
75
|
+
const configPath = join(basePath, configName);
|
|
76
|
+
const itemsToDownload = [];
|
|
77
|
+
if (!existsSync(binaryPath) && allowDownload) {
|
|
78
|
+
const binaryUrl = `${COLLECTOR_DOWNLOAD_BASE}/${binaryName}.gz`;
|
|
79
|
+
itemsToDownload.push({ url: binaryUrl, path: binaryPath, executable: true });
|
|
80
|
+
}
|
|
81
|
+
if (!existsSync(configPath) && allowDownload) {
|
|
82
|
+
const configUrl = `${COLLECTOR_DOWNLOAD_BASE}/${configName}.gz`;
|
|
83
|
+
itemsToDownload.push({ url: configUrl, path: configPath, executable: false });
|
|
84
|
+
}
|
|
85
|
+
for (const item of itemsToDownload) {
|
|
86
|
+
await downloadAndDecompressGzip(item.url, item.path, item.executable);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
binaryPath: existsSync(binaryPath) ? binaryPath : null,
|
|
90
|
+
configPath: existsSync(configPath) ? configPath : null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Discovers collector via UDP broadcast
|
|
95
|
+
*/
|
|
96
|
+
function discoverCollectorViaUDP(timeoutMs = 5000) {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
const socket = dgram.createSocket('udp4');
|
|
99
|
+
const discovered = [];
|
|
100
|
+
let timeout;
|
|
101
|
+
socket.on('message', (msg) => {
|
|
102
|
+
try {
|
|
103
|
+
const message = JSON.parse(msg.toString());
|
|
104
|
+
// Check if version matches
|
|
105
|
+
if (message.version === COLLECTOR_VERSION && message.status === 'READY') {
|
|
106
|
+
discovered.push(message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Ignore parse errors
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
socket.on('error', () => {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
socket.close();
|
|
116
|
+
resolve(null);
|
|
117
|
+
});
|
|
118
|
+
socket.bind(() => {
|
|
119
|
+
socket.setBroadcast(true);
|
|
120
|
+
timeout = setTimeout(() => {
|
|
121
|
+
socket.close();
|
|
122
|
+
// Return the first discovered collector
|
|
123
|
+
resolve(discovered.length > 0 ? discovered[0] : null);
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Checks if collector is already running via UDP discovery
|
|
130
|
+
*/
|
|
131
|
+
export async function isCollectorRunning() {
|
|
132
|
+
try {
|
|
133
|
+
const discovered = await discoverCollectorViaUDP(2000);
|
|
134
|
+
if (discovered) {
|
|
135
|
+
return {
|
|
136
|
+
isRunning: true,
|
|
137
|
+
isReady: discovered.status === 'READY',
|
|
138
|
+
pid: discovered.pid,
|
|
139
|
+
otlpEndpoint: discovered.otlpEndpoint,
|
|
140
|
+
version: discovered.version,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Discovery failed, collector probably not running
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
isRunning: false,
|
|
149
|
+
isReady: false,
|
|
150
|
+
pid: 0,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Starts the OpenTelemetry collector process
|
|
155
|
+
*/
|
|
156
|
+
export async function startCollector(logger, otlpEndpoint) {
|
|
157
|
+
const collectorInfo = await resolveCollector(true);
|
|
158
|
+
if (!collectorInfo.binaryPath) {
|
|
159
|
+
throw new Error('Collector binary not found and download failed');
|
|
160
|
+
}
|
|
161
|
+
if (!collectorInfo.configPath) {
|
|
162
|
+
throw new Error('Collector config not found and download failed');
|
|
163
|
+
}
|
|
164
|
+
// Determine OTLP endpoint
|
|
165
|
+
let localEndpoint = otlpEndpoint || 'localhost:4318';
|
|
166
|
+
localEndpoint = localEndpoint.replace(/^http:\/\//, '').replace(/^https:\/\//, '');
|
|
167
|
+
// Set environment variables for collector
|
|
168
|
+
const env = {
|
|
169
|
+
...process.env,
|
|
170
|
+
BEAM_OTLP_HTTP_ENDPOINT: localEndpoint,
|
|
171
|
+
BEAM_CLICKHOUSE_ENDPOINT: process.env.BEAM_CLICKHOUSE_ENDPOINT || '',
|
|
172
|
+
BEAM_CLICKHOUSE_USERNAME: process.env.BEAM_CLICKHOUSE_USERNAME || '',
|
|
173
|
+
BEAM_CLICKHOUSE_PASSWORD: process.env.BEAM_CLICKHOUSE_PASSWORD || '',
|
|
174
|
+
BEAM_COLLECTOR_DISCOVERY_PORT: String(DISCOVERY_PORT),
|
|
175
|
+
BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT: process.env.BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT || '5s',
|
|
176
|
+
BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE: process.env.BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE || '5000',
|
|
177
|
+
BEAM_CLICKHOUSE_EXPORTER_TIMEOUT: process.env.BEAM_CLICKHOUSE_EXPORTER_TIMEOUT || '5s',
|
|
178
|
+
BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE: process.env.BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE || '1000',
|
|
179
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED || 'true',
|
|
180
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL || '5s',
|
|
181
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL || '30s',
|
|
182
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME || '300s',
|
|
183
|
+
};
|
|
184
|
+
// Start collector process
|
|
185
|
+
const collectorProcess = spawn(collectorInfo.binaryPath, ['--config', collectorInfo.configPath], {
|
|
186
|
+
env,
|
|
187
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
188
|
+
detached: false,
|
|
189
|
+
});
|
|
190
|
+
collectorProcess.stdout?.on('data', (data) => {
|
|
191
|
+
logger.debug(`[Collector] ${data.toString().trim()}`);
|
|
192
|
+
});
|
|
193
|
+
collectorProcess.stderr?.on('data', (data) => {
|
|
194
|
+
logger.debug(`[Collector ERR] ${data.toString().trim()}`);
|
|
195
|
+
});
|
|
196
|
+
collectorProcess.on('error', (err) => {
|
|
197
|
+
logger.error(`[Collector] Failed to start: ${err.message}`);
|
|
198
|
+
});
|
|
199
|
+
collectorProcess.on('exit', (code) => {
|
|
200
|
+
logger.warn(`[Collector] Process exited with code ${code}`);
|
|
201
|
+
});
|
|
202
|
+
logger.info(`[Collector] Started with PID ${collectorProcess.pid}, endpoint: ${localEndpoint}`);
|
|
203
|
+
return {
|
|
204
|
+
process: collectorProcess,
|
|
205
|
+
endpoint: `http://${localEndpoint}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Discovers or starts the collector and returns the OTLP endpoint
|
|
210
|
+
*/
|
|
211
|
+
export async function discoverOrStartCollector(logger, standardOtelEnabled) {
|
|
212
|
+
if (!standardOtelEnabled) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
// First, check if collector is already running
|
|
216
|
+
const status = await isCollectorRunning();
|
|
217
|
+
if (status.isRunning && status.isReady && status.otlpEndpoint) {
|
|
218
|
+
logger.info(`[Collector] Found running collector at ${status.otlpEndpoint}`);
|
|
219
|
+
return `http://${status.otlpEndpoint}`;
|
|
220
|
+
}
|
|
221
|
+
// Collector not running - start it
|
|
222
|
+
try {
|
|
223
|
+
logger.info('[Collector] Starting OpenTelemetry collector...');
|
|
224
|
+
const { endpoint } = await startCollector(logger);
|
|
225
|
+
// Wait a bit for collector to start and become ready
|
|
226
|
+
// Try to discover it
|
|
227
|
+
for (let i = 0; i < DISCOVERY_ATTEMPTS; i++) {
|
|
228
|
+
await new Promise(resolve => setTimeout(resolve, DISCOVERY_DELAY));
|
|
229
|
+
const newStatus = await isCollectorRunning();
|
|
230
|
+
if (newStatus.isRunning && newStatus.isReady) {
|
|
231
|
+
logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint}`);
|
|
232
|
+
return newStatus.otlpEndpoint ? `http://${newStatus.otlpEndpoint}` : endpoint;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Return the endpoint we started with, even if discovery didn't find it yet
|
|
236
|
+
logger.warn('[Collector] Collector started but not yet ready, using configured endpoint');
|
|
237
|
+
return endpoint;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
logger.error(`[Collector] Failed to start collector: ${err instanceof Error ? err.message : String(err)}`);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=collector-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collector-manager.js","sourceRoot":"","sources":["../src/collector-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, ChildProcess } from 'child_process';\r\nimport { createWriteStream, existsSync, chmodSync, mkdirSync } from 'fs';\r\nimport { join } from 'path';\r\nimport { pipeline } from 'stream/promises';\r\nimport { createGunzip } from 'zlib';\r\nimport type { Logger } from 'pino';\r\nimport dgram from 'dgram';\r\n\r\ninterface CollectorDiscoveryEntry {\r\n version: string;\r\n status: string;\r\n pid: number;\r\n otlpEndpoint: string;\r\n}\r\n\r\ninterface CollectorStatus {\r\n isRunning: boolean;\r\n isReady: boolean;\r\n pid: number;\r\n otlpEndpoint?: string;\r\n version?: string;\r\n}\r\n\r\nconst COLLECTOR_VERSION = '0.0.123'; // Match C# collector version\r\nconst COLLECTOR_DOWNLOAD_BASE = `https://collectors.beamable.com/version/${COLLECTOR_VERSION}`;\r\nconst DISCOVERY_PORT = parseInt(process.env.BEAM_COLLECTOR_DISCOVERY_PORT || '8688', 10);\r\nconst DISCOVERY_DELAY = 100; // milliseconds\r\nconst DISCOVERY_ATTEMPTS = 10;\r\n\r\n/**\r\n * Gets the collector storage directory (similar to C# LocalApplicationData/beam/collectors/version)\r\n */\r\nfunction getCollectorStoragePath(): string {\r\n // Use temp directory for now - in containers this should be writable\r\n const tempDir = process.env.TMPDIR || process.env.TMP || '/tmp';\r\n return join(tempDir, 'beam', 'collectors', COLLECTOR_VERSION);\r\n}\r\n\r\n/**\r\n * Gets the collector binary name for the current platform\r\n */\r\nfunction getCollectorBinaryName(): string {\r\n const platform = process.platform;\r\n const arch = process.arch;\r\n \r\n if (platform === 'linux' && arch === 'x64') {\r\n return 'beamable-collector-linux-amd64';\r\n } else if (platform === 'linux' && arch === 'arm64') {\r\n return 'beamable-collector-linux-arm64';\r\n } else if (platform === 'darwin' && arch === 'x64') {\r\n return 'beamable-collector-darwin-amd64';\r\n } else if (platform === 'darwin' && arch === 'arm64') {\r\n return 'beamable-collector-darwin-arm64';\r\n } else if (platform === 'win32' && arch === 'x64') {\r\n return 'beamable-collector-windows-amd64.exe';\r\n }\r\n \r\n throw new Error(`Unsupported platform: ${platform} ${arch}`);\r\n}\r\n\r\n/**\r\n * Downloads and decompresses a gzipped file\r\n */\r\nasync function downloadAndDecompressGzip(url: string, outputPath: string, makeExecutable: boolean = false): Promise<void> {\r\n const response = await fetch(url);\r\n if (!response.ok) {\r\n throw new Error(`Failed to download ${url}: ${response.statusText}`);\r\n }\r\n \r\n const dir = require('path').dirname(outputPath);\r\n if (!existsSync(dir)) {\r\n mkdirSync(dir, { recursive: true });\r\n }\r\n \r\n const gunzip = createGunzip();\r\n const writeStream = createWriteStream(outputPath);\r\n \r\n await pipeline(response.body as any, gunzip, writeStream);\r\n \r\n if (makeExecutable && process.platform !== 'win32') {\r\n try {\r\n chmodSync(outputPath, 0o755);\r\n } catch (error) {\r\n console.error(`Failed to make ${outputPath} executable:`, error);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Resolves the collector binary and config, downloading if needed\r\n */\r\nasync function resolveCollector(allowDownload: boolean = true): Promise<{ binaryPath: string | null; configPath: string | null }> {\r\n const basePath = getCollectorStoragePath();\r\n const binaryName = getCollectorBinaryName();\r\n const configName = 'clickhouse-config.yaml';\r\n \r\n const binaryPath = join(basePath, binaryName);\r\n const configPath = join(basePath, configName);\r\n \r\n const itemsToDownload: Array<{ url: string; path: string; executable: boolean }> = [];\r\n \r\n if (!existsSync(binaryPath) && allowDownload) {\r\n const binaryUrl = `${COLLECTOR_DOWNLOAD_BASE}/${binaryName}.gz`;\r\n itemsToDownload.push({ url: binaryUrl, path: binaryPath, executable: true });\r\n }\r\n \r\n if (!existsSync(configPath) && allowDownload) {\r\n const configUrl = `${COLLECTOR_DOWNLOAD_BASE}/${configName}.gz`;\r\n itemsToDownload.push({ url: configUrl, path: configPath, executable: false });\r\n }\r\n \r\n for (const item of itemsToDownload) {\r\n await downloadAndDecompressGzip(item.url, item.path, item.executable);\r\n }\r\n \r\n return {\r\n binaryPath: existsSync(binaryPath) ? binaryPath : null,\r\n configPath: existsSync(configPath) ? configPath : null,\r\n };\r\n}\r\n\r\n/**\r\n * Discovers collector via UDP broadcast\r\n */\r\nfunction discoverCollectorViaUDP(timeoutMs: number = 5000): Promise<CollectorDiscoveryEntry | null> {\r\n return new Promise((resolve) => {\r\n const socket = dgram.createSocket('udp4');\r\n const discovered: CollectorDiscoveryEntry[] = [];\r\n let timeout: NodeJS.Timeout;\r\n \r\n socket.on('message', (msg) => {\r\n try {\r\n const message = JSON.parse(msg.toString()) as CollectorDiscoveryEntry;\r\n // Check if version matches\r\n if (message.version === COLLECTOR_VERSION && message.status === 'READY') {\r\n discovered.push(message);\r\n }\r\n } catch (error) {\r\n // Ignore parse errors\r\n }\r\n });\r\n \r\n socket.on('error', () => {\r\n clearTimeout(timeout);\r\n socket.close();\r\n resolve(null);\r\n });\r\n \r\n socket.bind(() => {\r\n socket.setBroadcast(true);\r\n \r\n timeout = setTimeout(() => {\r\n socket.close();\r\n // Return the first discovered collector\r\n resolve(discovered.length > 0 ? discovered[0] : null);\r\n }, timeoutMs);\r\n });\r\n });\r\n}\r\n\r\n/**\r\n * Checks if collector is already running via UDP discovery\r\n */\r\nexport async function isCollectorRunning(): Promise<CollectorStatus> {\r\n try {\r\n const discovered = await discoverCollectorViaUDP(2000);\r\n if (discovered) {\r\n return {\r\n isRunning: true,\r\n isReady: discovered.status === 'READY',\r\n pid: discovered.pid,\r\n otlpEndpoint: discovered.otlpEndpoint,\r\n version: discovered.version,\r\n };\r\n }\r\n } catch (error) {\r\n // Discovery failed, collector probably not running\r\n }\r\n \r\n return {\r\n isRunning: false,\r\n isReady: false,\r\n pid: 0,\r\n };\r\n}\r\n\r\n/**\r\n * Starts the OpenTelemetry collector process\r\n */\r\nexport async function startCollector(\r\n logger: Logger,\r\n otlpEndpoint?: string\r\n): Promise<{ process: ChildProcess; endpoint: string }> {\r\n const collectorInfo = await resolveCollector(true);\r\n \r\n if (!collectorInfo.binaryPath) {\r\n throw new Error('Collector binary not found and download failed');\r\n }\r\n \r\n if (!collectorInfo.configPath) {\r\n throw new Error('Collector config not found and download failed');\r\n }\r\n \r\n // Determine OTLP endpoint\r\n let localEndpoint = otlpEndpoint || 'localhost:4318';\r\n localEndpoint = localEndpoint.replace(/^http:\\/\\//, '').replace(/^https:\\/\\//, '');\r\n \r\n // Set environment variables for collector\r\n const env = {\r\n ...process.env,\r\n BEAM_OTLP_HTTP_ENDPOINT: localEndpoint,\r\n BEAM_CLICKHOUSE_ENDPOINT: process.env.BEAM_CLICKHOUSE_ENDPOINT || '',\r\n BEAM_CLICKHOUSE_USERNAME: process.env.BEAM_CLICKHOUSE_USERNAME || '',\r\n BEAM_CLICKHOUSE_PASSWORD: process.env.BEAM_CLICKHOUSE_PASSWORD || '',\r\n BEAM_COLLECTOR_DISCOVERY_PORT: String(DISCOVERY_PORT),\r\n BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT: process.env.BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT || '5s',\r\n BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE: process.env.BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE || '5000',\r\n BEAM_CLICKHOUSE_EXPORTER_TIMEOUT: process.env.BEAM_CLICKHOUSE_EXPORTER_TIMEOUT || '5s',\r\n BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE: process.env.BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE || '1000',\r\n BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED || 'true',\r\n BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL || '5s',\r\n BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL || '30s',\r\n BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME || '300s',\r\n };\r\n \r\n // Start collector process\r\n const collectorProcess = spawn(collectorInfo.binaryPath, ['--config', collectorInfo.configPath], {\r\n env,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n detached: false,\r\n });\r\n \r\n collectorProcess.stdout?.on('data', (data) => {\r\n logger.debug(`[Collector] ${data.toString().trim()}`);\r\n });\r\n \r\n collectorProcess.stderr?.on('data', (data) => {\r\n logger.debug(`[Collector ERR] ${data.toString().trim()}`);\r\n });\r\n \r\n collectorProcess.on('error', (err) => {\r\n logger.error(`[Collector] Failed to start: ${err.message}`);\r\n });\r\n \r\n collectorProcess.on('exit', (code) => {\r\n logger.warn(`[Collector] Process exited with code ${code}`);\r\n });\r\n \r\n logger.info(`[Collector] Started with PID ${collectorProcess.pid}, endpoint: ${localEndpoint}`);\r\n \r\n return {\r\n process: collectorProcess,\r\n endpoint: `http://${localEndpoint}`,\r\n };\r\n}\r\n\r\n/**\r\n * Discovers or starts the collector and returns the OTLP endpoint\r\n */\r\nexport async function discoverOrStartCollector(\r\n logger: Logger,\r\n standardOtelEnabled: boolean\r\n): Promise<string | null> {\r\n if (!standardOtelEnabled) {\r\n return null;\r\n }\r\n \r\n // First, check if collector is already running\r\n const status = await isCollectorRunning();\r\n if (status.isRunning && status.isReady && status.otlpEndpoint) {\r\n logger.info(`[Collector] Found running collector at ${status.otlpEndpoint}`);\r\n return `http://${status.otlpEndpoint}`;\r\n }\r\n \r\n // Collector not running - start it\r\n try {\r\n logger.info('[Collector] Starting OpenTelemetry collector...');\r\n const { endpoint } = await startCollector(logger);\r\n \r\n // Wait a bit for collector to start and become ready\r\n // Try to discover it\r\n for (let i = 0; i < DISCOVERY_ATTEMPTS; i++) {\r\n await new Promise(resolve => setTimeout(resolve, DISCOVERY_DELAY));\r\n const newStatus = await isCollectorRunning();\r\n if (newStatus.isRunning && newStatus.isReady) {\r\n logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint}`);\r\n return newStatus.otlpEndpoint ? `http://${newStatus.otlpEndpoint}` : endpoint;\r\n }\r\n }\r\n \r\n // Return the endpoint we started with, even if discovery didn't find it yet\r\n logger.warn('[Collector] Collector started but not yet ready, using configured endpoint');\r\n return endpoint;\r\n } catch (err) {\r\n logger.error(`[Collector] Failed to start collector: ${err instanceof Error ? err.message : String(err)}`);\r\n return null;\r\n }\r\n}\r\n\r\n"]}
|