@omen.foundation/node-microservice-runtime 0.1.76 → 0.1.77
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/auth.cjs +97 -0
- package/dist/cli/commands/scaffold.js +17 -1
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/collector-manager.cjs +957 -0
- package/dist/decorators.cjs +181 -0
- package/dist/dependency.cjs +165 -0
- package/dist/dev.cjs +34 -0
- package/dist/discovery.cjs +79 -0
- package/dist/docs.cjs +206 -0
- package/dist/env-loader.cjs +187 -0
- package/dist/env.cjs +125 -0
- package/dist/errors.cjs +58 -0
- package/dist/federation.cjs +356 -0
- package/dist/index.cjs +66 -0
- package/dist/inventory.cjs +361 -0
- package/dist/logger.cjs +545 -0
- package/dist/message.cjs +19 -0
- package/dist/requester.cjs +100 -0
- package/dist/routing.cjs +39 -0
- package/dist/runtime.cjs +841 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +63 -8
- package/dist/runtime.js.map +1 -1
- package/dist/services.cjs +356 -0
- package/dist/storage.cjs +147 -0
- package/dist/types.cjs +2 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/urls.cjs +55 -0
- package/dist/websocket.cjs +142 -0
- package/package.json +1 -1
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.fetchClickHouseCredentials = fetchClickHouseCredentials;
|
|
40
|
+
exports.getClickHouseCredentialsStatus = getClickHouseCredentialsStatus;
|
|
41
|
+
exports.isCollectorRunning = isCollectorRunning;
|
|
42
|
+
exports.addAuthEnvironmentVars = addAuthEnvironmentVars;
|
|
43
|
+
exports.startCollectorAndWaitForReady = startCollectorAndWaitForReady;
|
|
44
|
+
exports.startCollectorAsync = startCollectorAsync;
|
|
45
|
+
exports.setupCollectorBeforeLogging = setupCollectorBeforeLogging;
|
|
46
|
+
exports.startCollector = startCollector;
|
|
47
|
+
exports.getCollectorProcessStatus = getCollectorProcessStatus;
|
|
48
|
+
exports.discoverOrStartCollector = discoverOrStartCollector;
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const fs_1 = require("fs");
|
|
51
|
+
const path_1 = require("path");
|
|
52
|
+
const promises_1 = require("stream/promises");
|
|
53
|
+
const zlib_1 = require("zlib");
|
|
54
|
+
const node_crypto_1 = require("node:crypto");
|
|
55
|
+
const net_1 = require("net");
|
|
56
|
+
const pino_1 = __importDefault(require("pino"));
|
|
57
|
+
const dgram_1 = __importDefault(require("dgram"));
|
|
58
|
+
const deasync_1 = __importDefault(require("deasync"));
|
|
59
|
+
const urls_js_1 = require("./utils/urls.js");
|
|
60
|
+
const COLLECTOR_VERSION = '1.0.1';
|
|
61
|
+
const COLLECTOR_DOWNLOAD_BASE = `https://collectors.beamable.com/version/${COLLECTOR_VERSION}`;
|
|
62
|
+
const DISCOVERY_PORT = parseInt(process.env.BEAM_COLLECTOR_DISCOVERY_PORT || '8688', 10);
|
|
63
|
+
function findFreePort() {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const server = (0, net_1.createServer)();
|
|
66
|
+
server.listen(0, () => {
|
|
67
|
+
const address = server.address();
|
|
68
|
+
const port = typeof address === 'object' && address !== null ? address.port : 0;
|
|
69
|
+
server.close(() => {
|
|
70
|
+
if (port > 0) {
|
|
71
|
+
resolve(port);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
resolve(9090);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
server.on('error', (err) => {
|
|
79
|
+
reject(err);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
let globalCollectorProcess = null;
|
|
84
|
+
let globalCollectorStartError = null;
|
|
85
|
+
let globalCollectorExitCode = null;
|
|
86
|
+
let globalCollectorStderr = [];
|
|
87
|
+
let globalCollectorStartupPromise = null;
|
|
88
|
+
let globalCollectorInitError = null;
|
|
89
|
+
function calculateSignature(pid, secret, uriPathAndQuery, body = null, version = '1') {
|
|
90
|
+
let dataToSign = `${secret}${pid}${version}${uriPathAndQuery}`;
|
|
91
|
+
if (body) {
|
|
92
|
+
dataToSign += body;
|
|
93
|
+
}
|
|
94
|
+
const hash = (0, node_crypto_1.createHash)('md5').update(dataToSign, 'utf8').digest('base64');
|
|
95
|
+
return hash;
|
|
96
|
+
}
|
|
97
|
+
function getPathAndQuery(url) {
|
|
98
|
+
try {
|
|
99
|
+
const urlObj = new URL(url);
|
|
100
|
+
return urlObj.pathname + urlObj.search;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return url;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function fetchClickHouseCredentials(env) {
|
|
107
|
+
const apiUrl = (0, urls_js_1.hostToHttpUrl)(env.host);
|
|
108
|
+
const uriPath = '/api/beamo/otel/auth/writer/config';
|
|
109
|
+
const configUrl = new URL(uriPath, apiUrl).toString();
|
|
110
|
+
const verboseLogger = (0, pino_1.default)({ name: 'fetch-credentials', level: 'info' }, process.stdout);
|
|
111
|
+
verboseLogger.info(`[Credentials] Starting credential fetch from ${configUrl}...`);
|
|
112
|
+
const secret = process.env.SECRET;
|
|
113
|
+
if (!secret) {
|
|
114
|
+
verboseLogger.error('[Credentials] SECRET environment variable is missing');
|
|
115
|
+
throw new Error('SECRET environment variable is required to fetch ClickHouse credentials');
|
|
116
|
+
}
|
|
117
|
+
verboseLogger.info('[Credentials] SECRET found, calculating signature...');
|
|
118
|
+
const pathAndQuery = getPathAndQuery(uriPath);
|
|
119
|
+
const signature = calculateSignature(env.pid, secret, pathAndQuery, null, '1');
|
|
120
|
+
verboseLogger.info('[Credentials] Signature calculated, making API request...');
|
|
121
|
+
const headers = {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
Accept: 'application/json',
|
|
124
|
+
'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,
|
|
125
|
+
'X-BEAM-SIGNATURE': signature,
|
|
126
|
+
};
|
|
127
|
+
const fetchStartTime = Date.now();
|
|
128
|
+
const response = await fetch(configUrl, {
|
|
129
|
+
method: 'GET',
|
|
130
|
+
headers,
|
|
131
|
+
});
|
|
132
|
+
const fetchElapsed = Date.now() - fetchStartTime;
|
|
133
|
+
verboseLogger.info(`[Credentials] API request completed in ${fetchElapsed}ms, status: ${response.status}`);
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
136
|
+
verboseLogger.error(`[Credentials] API request failed: ${response.status} ${response.statusText}`);
|
|
137
|
+
throw new Error(`Failed to fetch ClickHouse credentials from ${configUrl}: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);
|
|
138
|
+
}
|
|
139
|
+
verboseLogger.info('[Credentials] Parsing response JSON...');
|
|
140
|
+
const credentials = await response.json();
|
|
141
|
+
if (!credentials.endpoint || !credentials.username || !credentials.password) {
|
|
142
|
+
verboseLogger.error('[Credentials] Invalid response: missing required fields');
|
|
143
|
+
throw new Error('Invalid ClickHouse credentials response: missing required fields');
|
|
144
|
+
}
|
|
145
|
+
verboseLogger.info(`[Credentials] Successfully fetched credentials (endpoint: ${credentials.endpoint}, username: ${credentials.username})`);
|
|
146
|
+
return credentials;
|
|
147
|
+
}
|
|
148
|
+
function getCollectorStoragePath() {
|
|
149
|
+
const productionPath = '/opt/beam/collectors';
|
|
150
|
+
const productionVersionPath = (0, path_1.join)(productionPath, COLLECTOR_VERSION);
|
|
151
|
+
if ((0, fs_1.existsSync)(productionPath)) {
|
|
152
|
+
return productionVersionPath;
|
|
153
|
+
}
|
|
154
|
+
const tempDir = process.env.TMPDIR || process.env.TMP || '/tmp';
|
|
155
|
+
return (0, path_1.join)(tempDir, 'beam', 'collectors', COLLECTOR_VERSION);
|
|
156
|
+
}
|
|
157
|
+
function getCollectorBinaryName() {
|
|
158
|
+
const platform = process.platform;
|
|
159
|
+
const arch = process.arch;
|
|
160
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
161
|
+
return 'collector-linux-amd64';
|
|
162
|
+
}
|
|
163
|
+
else if (platform === 'linux' && arch === 'arm64') {
|
|
164
|
+
return 'collector-linux-arm64';
|
|
165
|
+
}
|
|
166
|
+
else if (platform === 'darwin' && arch === 'x64') {
|
|
167
|
+
return 'collector-darwin-amd64';
|
|
168
|
+
}
|
|
169
|
+
else if (platform === 'darwin' && arch === 'arm64') {
|
|
170
|
+
return 'collector-darwin-arm64';
|
|
171
|
+
}
|
|
172
|
+
else if (platform === 'win32' && arch === 'x64') {
|
|
173
|
+
return 'collector-windows-amd64.exe';
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
176
|
+
}
|
|
177
|
+
async function downloadAndDecompressGzip(url, outputPath, makeExecutable = false) {
|
|
178
|
+
var _a;
|
|
179
|
+
let response;
|
|
180
|
+
try {
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
183
|
+
try {
|
|
184
|
+
response = await fetch(url, {
|
|
185
|
+
signal: controller.signal,
|
|
186
|
+
headers: {
|
|
187
|
+
'User-Agent': 'Beamable-Node-Microservice-Runtime/1.0',
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
clearTimeout(timeoutId);
|
|
191
|
+
}
|
|
192
|
+
catch (fetchError) {
|
|
193
|
+
clearTimeout(timeoutId);
|
|
194
|
+
if (fetchError instanceof Error) {
|
|
195
|
+
let underlyingError = '';
|
|
196
|
+
if (fetchError.cause) {
|
|
197
|
+
const cause = fetchError.cause;
|
|
198
|
+
if (cause instanceof Error) {
|
|
199
|
+
const causeCode = cause.code;
|
|
200
|
+
underlyingError = ` (cause: ${cause.name} - ${cause.message}${causeCode ? ` [code: ${causeCode}]` : ''})`;
|
|
201
|
+
}
|
|
202
|
+
else if (typeof cause === 'object' && cause.code) {
|
|
203
|
+
underlyingError = ` (cause code: ${cause.code})`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (fetchError.name === 'AbortError') {
|
|
207
|
+
throw new Error(`Timeout downloading ${url} (30s limit exceeded)${underlyingError}`);
|
|
208
|
+
}
|
|
209
|
+
const errorCode = fetchError.code || ((_a = fetchError.cause) === null || _a === void 0 ? void 0 : _a.code);
|
|
210
|
+
if (errorCode) {
|
|
211
|
+
if (errorCode === 'ENOTFOUND' || errorCode === 'EAI_AGAIN') {
|
|
212
|
+
throw new Error(`DNS resolution failed for ${url} (code: ${errorCode}). Check network connectivity and DNS settings.`);
|
|
213
|
+
}
|
|
214
|
+
if (errorCode === 'ECONNREFUSED') {
|
|
215
|
+
throw new Error(`Connection refused to ${url} (code: ${errorCode}). Server may be down or firewall blocking.`);
|
|
216
|
+
}
|
|
217
|
+
if (errorCode === 'ETIMEDOUT' || errorCode === 'ECONNRESET') {
|
|
218
|
+
throw new Error(`Connection timeout/reset to ${url} (code: ${errorCode}). Network may be slow or unreachable.`);
|
|
219
|
+
}
|
|
220
|
+
if (errorCode === 'CERT_HAS_EXPIRED' || errorCode === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
221
|
+
throw new Error(`SSL/TLS certificate error connecting to ${url} (code: ${errorCode}): ${fetchError.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const errorMsg = fetchError.message.toLowerCase();
|
|
225
|
+
if (errorMsg.includes('enotfound') || errorMsg.includes('dns')) {
|
|
226
|
+
throw new Error(`DNS resolution failed for ${url}. Check network connectivity and DNS settings.${underlyingError}`);
|
|
227
|
+
}
|
|
228
|
+
if (errorMsg.includes('econnrefused') || errorMsg.includes('connection refused')) {
|
|
229
|
+
throw new Error(`Connection refused to ${url}. Server may be down or firewall blocking.${underlyingError}`);
|
|
230
|
+
}
|
|
231
|
+
if (errorMsg.includes('etimedout') || errorMsg.includes('timeout')) {
|
|
232
|
+
throw new Error(`Connection timeout to ${url}. Network may be slow or unreachable.${underlyingError}`);
|
|
233
|
+
}
|
|
234
|
+
if (errorMsg.includes('certificate') || errorMsg.includes('ssl') || errorMsg.includes('tls')) {
|
|
235
|
+
throw new Error(`SSL/TLS error connecting to ${url}: ${fetchError.message}${underlyingError}`);
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`Network error downloading ${url}: ${fetchError.name} - ${fetchError.message}${underlyingError}`);
|
|
238
|
+
}
|
|
239
|
+
throw new Error(`Network error downloading ${url}: ${String(fetchError)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (error instanceof Error) {
|
|
244
|
+
if (error.message.includes('Timeout') || error.message.includes('DNS') || error.message.includes('Connection')) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Network error downloading ${url}: ${error.message}`);
|
|
248
|
+
}
|
|
249
|
+
throw new Error(`Network error downloading ${url}: ${String(error)}`);
|
|
250
|
+
}
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
const errorText = await response.text().catch(() => '');
|
|
253
|
+
throw new Error(`HTTP ${response.status} downloading ${url}: ${response.statusText}${errorText ? ` - ${errorText.substring(0, 200)}` : ''}`);
|
|
254
|
+
}
|
|
255
|
+
const dir = (0, path_1.dirname)(outputPath);
|
|
256
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
257
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
const gunzip = (0, zlib_1.createGunzip)();
|
|
260
|
+
const writeStream = (0, fs_1.createWriteStream)(outputPath);
|
|
261
|
+
await (0, promises_1.pipeline)(response.body, gunzip, writeStream);
|
|
262
|
+
if (makeExecutable && process.platform !== 'win32') {
|
|
263
|
+
try {
|
|
264
|
+
(0, fs_1.chmodSync)(outputPath, 0o755);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
console.error(`Failed to make ${outputPath} executable:`, error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function resolveCollector(allowDownload = true, logger) {
|
|
272
|
+
const basePath = getCollectorStoragePath();
|
|
273
|
+
const binaryName = getCollectorBinaryName();
|
|
274
|
+
const configName = 'clickhouse-config.yaml';
|
|
275
|
+
const binaryPath = (0, path_1.join)(basePath, binaryName);
|
|
276
|
+
const configPath = (0, path_1.join)(basePath, configName);
|
|
277
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Resolving collector - basePath: ${basePath}, binaryName: ${binaryName}`);
|
|
278
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Checking binary at: ${binaryPath}`);
|
|
279
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Checking config at: ${configPath}`);
|
|
280
|
+
const baseDirExists = (0, fs_1.existsSync)(basePath);
|
|
281
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Base directory exists: ${baseDirExists}`);
|
|
282
|
+
if (baseDirExists) {
|
|
283
|
+
try {
|
|
284
|
+
const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
|
285
|
+
const dirContents = await fsPromises.readdir(basePath).catch(() => []);
|
|
286
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Base directory contents: ${dirContents.join(', ') || '(empty)'}`);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`[Collector] Could not read base directory: ${e instanceof Error ? e.message : String(e)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const binaryExists = (0, fs_1.existsSync)(binaryPath);
|
|
293
|
+
const configExists = (0, fs_1.existsSync)(configPath);
|
|
294
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Binary exists: ${binaryExists}, Config exists: ${configExists}`);
|
|
295
|
+
const itemsToDownload = [];
|
|
296
|
+
if (!binaryExists && allowDownload) {
|
|
297
|
+
const binaryUrl = `${COLLECTOR_DOWNLOAD_BASE}/${binaryName}.gz`;
|
|
298
|
+
itemsToDownload.push({ url: binaryUrl, path: binaryPath, executable: true });
|
|
299
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`[Collector] Binary not found at ${binaryPath}, will download from: ${binaryUrl}`);
|
|
300
|
+
}
|
|
301
|
+
else if (binaryExists) {
|
|
302
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Binary found at: ${binaryPath}`);
|
|
303
|
+
}
|
|
304
|
+
else if (!allowDownload) {
|
|
305
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`[Collector] Binary not found at ${binaryPath} and downloads are disabled`);
|
|
306
|
+
}
|
|
307
|
+
if (!configExists && allowDownload) {
|
|
308
|
+
const configUrl = `${COLLECTOR_DOWNLOAD_BASE}/${configName}.gz`;
|
|
309
|
+
itemsToDownload.push({ url: configUrl, path: configPath, executable: false });
|
|
310
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`[Collector] Config not found at ${configPath}, will download from: ${configUrl}`);
|
|
311
|
+
}
|
|
312
|
+
else if (configExists) {
|
|
313
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Config found at: ${configPath}`);
|
|
314
|
+
}
|
|
315
|
+
else if (!allowDownload) {
|
|
316
|
+
logger === null || logger === void 0 ? void 0 : logger.warn(`[Collector] Config not found at ${configPath} and downloads are disabled`);
|
|
317
|
+
}
|
|
318
|
+
if (itemsToDownload.length > 0) {
|
|
319
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Downloading ${itemsToDownload.length} item(s) in parallel...`);
|
|
320
|
+
const downloadPromises = itemsToDownload.map(async (item) => {
|
|
321
|
+
try {
|
|
322
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Downloading ${item.url}...`);
|
|
323
|
+
await downloadAndDecompressGzip(item.url, item.path, item.executable);
|
|
324
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Downloaded to ${item.path}`);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
328
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[Collector] Failed to download ${item.url}: ${errorMsg}`);
|
|
329
|
+
if (error instanceof Error && errorMsg.includes(item.url)) {
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
throw new Error(`Failed to download collector ${item.executable ? 'binary' : 'config'} from ${item.url}: ${errorMsg}`);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
await Promise.all(downloadPromises);
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
binaryPath: (0, fs_1.existsSync)(binaryPath) ? binaryPath : null,
|
|
339
|
+
configPath: (0, fs_1.existsSync)(configPath) ? configPath : null,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function discoverCollectorViaUDP(timeoutMs = 1000) {
|
|
343
|
+
return new Promise((resolve) => {
|
|
344
|
+
const socket = dgram_1.default.createSocket('udp4');
|
|
345
|
+
const discovered = [];
|
|
346
|
+
let timeout;
|
|
347
|
+
let resolved = false;
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
const safetyTimeout = setTimeout(() => {
|
|
350
|
+
if (!resolved) {
|
|
351
|
+
console.log(`[UDP Discovery] Safety timeout triggered - forcing resolution`);
|
|
352
|
+
resolved = true;
|
|
353
|
+
try {
|
|
354
|
+
socket.removeAllListeners();
|
|
355
|
+
socket.close();
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
}
|
|
359
|
+
clearTimeout(timeout);
|
|
360
|
+
resolve(discovered.length > 0 ? discovered[0] : null);
|
|
361
|
+
}
|
|
362
|
+
}, timeoutMs + 100);
|
|
363
|
+
const doResolve = (result) => {
|
|
364
|
+
if (!resolved) {
|
|
365
|
+
resolved = true;
|
|
366
|
+
clearTimeout(timeout);
|
|
367
|
+
clearTimeout(safetyTimeout);
|
|
368
|
+
try {
|
|
369
|
+
socket.removeAllListeners();
|
|
370
|
+
socket.close();
|
|
371
|
+
}
|
|
372
|
+
catch (e) {
|
|
373
|
+
}
|
|
374
|
+
console.log(`[UDP Discovery] Resolving promise with result: ${result ? 'found' : 'null'}`);
|
|
375
|
+
resolve(result);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
socket.on('message', (msg) => {
|
|
379
|
+
try {
|
|
380
|
+
const message = JSON.parse(msg.toString());
|
|
381
|
+
if (message.version === COLLECTOR_VERSION && message.status === 'READY') {
|
|
382
|
+
discovered.push(message);
|
|
383
|
+
const elapsed = Date.now() - startTime;
|
|
384
|
+
console.log(`[UDP Discovery] Found collector after ${elapsed}ms: ${message.otlpEndpoint}`);
|
|
385
|
+
doResolve(discovered[0]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
socket.on('error', (err) => {
|
|
392
|
+
const elapsed = Date.now() - startTime;
|
|
393
|
+
console.log(`[UDP Discovery] Socket error after ${elapsed}ms: ${err.message}`);
|
|
394
|
+
doResolve(null);
|
|
395
|
+
});
|
|
396
|
+
socket.bind(() => {
|
|
397
|
+
socket.setBroadcast(true);
|
|
398
|
+
console.log(`[UDP Discovery] Listening for collector broadcasts (timeout: ${timeoutMs}ms)...`);
|
|
399
|
+
timeout = setTimeout(() => {
|
|
400
|
+
const elapsed = Date.now() - startTime;
|
|
401
|
+
if (discovered.length > 0) {
|
|
402
|
+
console.log(`[UDP Discovery] Timeout after ${elapsed}ms, found ${discovered.length} collector(s)`);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
console.log(`[UDP Discovery] Timeout after ${elapsed}ms, no collector found`);
|
|
406
|
+
}
|
|
407
|
+
doResolve(discovered.length > 0 ? discovered[0] : null);
|
|
408
|
+
}, timeoutMs);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function getClickHouseCredentialsStatus() {
|
|
413
|
+
const hasEndpoint = !!process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
414
|
+
const hasUsername = !!process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
415
|
+
const hasPassword = !!process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
416
|
+
if (hasEndpoint && hasUsername && hasPassword) {
|
|
417
|
+
return {
|
|
418
|
+
hasEndpoint: true,
|
|
419
|
+
hasUsername: true,
|
|
420
|
+
hasPassword: true,
|
|
421
|
+
source: 'environment',
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
hasEndpoint,
|
|
426
|
+
hasUsername,
|
|
427
|
+
hasPassword,
|
|
428
|
+
source: 'missing',
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function isCollectorRunning() {
|
|
432
|
+
if (globalCollectorProcess) {
|
|
433
|
+
try {
|
|
434
|
+
const processAlive = globalCollectorProcess.exitCode === null &&
|
|
435
|
+
globalCollectorProcess.killed === false;
|
|
436
|
+
if (processAlive) {
|
|
437
|
+
const stderrText = globalCollectorStderr.join('\n');
|
|
438
|
+
const isReady = stderrText.includes('Everything is ready') ||
|
|
439
|
+
stderrText.includes('Begin running and processing data');
|
|
440
|
+
if (isReady) {
|
|
441
|
+
try {
|
|
442
|
+
const discovered = await discoverCollectorViaUDP(1000);
|
|
443
|
+
if (discovered && discovered.status === 'READY') {
|
|
444
|
+
return {
|
|
445
|
+
isRunning: true,
|
|
446
|
+
isReady: true,
|
|
447
|
+
pid: discovered.pid || globalCollectorProcess.pid || 0,
|
|
448
|
+
otlpEndpoint: discovered.otlpEndpoint,
|
|
449
|
+
version: discovered.version,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
}
|
|
455
|
+
const configuredEndpoint = process.env.BEAM_OTLP_HTTP_ENDPOINT || '0.0.0.0:4318';
|
|
456
|
+
return {
|
|
457
|
+
isRunning: true,
|
|
458
|
+
isReady: true,
|
|
459
|
+
pid: globalCollectorProcess.pid || 0,
|
|
460
|
+
otlpEndpoint: configuredEndpoint,
|
|
461
|
+
version: COLLECTOR_VERSION,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const udpStartTime = Date.now();
|
|
471
|
+
console.log('[UDP Discovery] Starting UDP discovery in isCollectorRunning()...');
|
|
472
|
+
console.log('[UDP Discovery] Calling discoverCollectorViaUDP(1000)...');
|
|
473
|
+
const discovered = await discoverCollectorViaUDP(1000);
|
|
474
|
+
const udpElapsed = Date.now() - udpStartTime;
|
|
475
|
+
console.log(`[UDP Discovery] UDP discovery promise resolved after ${udpElapsed}ms, result: ${discovered ? 'found' : 'null'}`);
|
|
476
|
+
if (discovered) {
|
|
477
|
+
console.log(`[UDP Discovery] Found collector in ${udpElapsed}ms: ${discovered.otlpEndpoint}`);
|
|
478
|
+
return {
|
|
479
|
+
isRunning: true,
|
|
480
|
+
isReady: discovered.status === 'READY',
|
|
481
|
+
pid: discovered.pid,
|
|
482
|
+
otlpEndpoint: discovered.otlpEndpoint,
|
|
483
|
+
version: discovered.version,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
console.log(`[UDP Discovery] No collector found after ${udpElapsed}ms, returning not running status`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
492
|
+
console.log(`[UDP Discovery] Error in isCollectorRunning(): ${errorMsg}`);
|
|
493
|
+
if (error instanceof Error && error.stack) {
|
|
494
|
+
console.log(`[UDP Discovery] Error stack: ${error.stack}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
console.log('[UDP Discovery] Returning default not-running status from isCollectorRunning()');
|
|
498
|
+
return {
|
|
499
|
+
isRunning: false,
|
|
500
|
+
isReady: false,
|
|
501
|
+
pid: 0,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function addCollectorConfigurationToEnvironment() {
|
|
505
|
+
const defaults = {
|
|
506
|
+
BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT: '5s',
|
|
507
|
+
BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE: '5000',
|
|
508
|
+
BEAM_CLICKHOUSE_EXPORTER_TIMEOUT: '5s',
|
|
509
|
+
BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE: '1000',
|
|
510
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED: 'true',
|
|
511
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL: '5s',
|
|
512
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL: '30s',
|
|
513
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME: '300s',
|
|
514
|
+
};
|
|
515
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
516
|
+
if (!process.env[key]) {
|
|
517
|
+
process.env[key] = defaultValue;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function addAuthEnvironmentVars(endpoint, username, password) {
|
|
522
|
+
process.env.BEAM_CLICKHOUSE_ENDPOINT = endpoint;
|
|
523
|
+
process.env.BEAM_CLICKHOUSE_USERNAME = username;
|
|
524
|
+
process.env.BEAM_CLICKHOUSE_PASSWORD = password;
|
|
525
|
+
if (!process.env.BEAM_CLICKHOUSE_ENDPOINT || !process.env.BEAM_CLICKHOUSE_USERNAME || !process.env.BEAM_CLICKHOUSE_PASSWORD) {
|
|
526
|
+
throw new Error(`Failed to set ClickHouse credentials in process.env - this should never happen in Node.js`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function startCollectorAndWaitForReady(env, _timeoutMs) {
|
|
530
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
531
|
+
const useLocalOtel = !!process.env.BEAM_LOCAL_OTEL;
|
|
532
|
+
const standardOtelEnabled = (isInDocker || useLocalOtel) && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
533
|
+
const hasExplicitEndpoint = !!process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
534
|
+
if (!standardOtelEnabled && !hasExplicitEndpoint) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const initLogger = (0, pino_1.default)({
|
|
538
|
+
name: 'beamable-otlp-init',
|
|
539
|
+
level: 'info',
|
|
540
|
+
}, process.stdout);
|
|
541
|
+
initLogger.info('[OTLP] Setting up collector (waiting for readiness before enabling Portal logs)...');
|
|
542
|
+
const setupStartTime = Date.now();
|
|
543
|
+
try {
|
|
544
|
+
initLogger.info('[OTLP] Step 1: Discovering or starting collector...');
|
|
545
|
+
const endpoint = await discoverOrStartCollector(initLogger, standardOtelEnabled, env);
|
|
546
|
+
const elapsed = Date.now() - setupStartTime;
|
|
547
|
+
if (endpoint) {
|
|
548
|
+
initLogger.info(`[OTLP] Collector ready at ${endpoint}. Portal logs now enabled. (took ${elapsed}ms)`);
|
|
549
|
+
return endpoint;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
initLogger.warn(`[OTLP] Collector setup failed, continuing without Portal logs. (took ${elapsed}ms)`);
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
const elapsed = Date.now() - setupStartTime;
|
|
558
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
559
|
+
initLogger.error(`[OTLP] Collector setup failed after ${elapsed}ms: ${errorMsg}`);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function startCollectorAsync(env) {
|
|
564
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
565
|
+
const useLocalOtel = !!process.env.BEAM_LOCAL_OTEL;
|
|
566
|
+
const standardOtelEnabled = (isInDocker || useLocalOtel) && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
567
|
+
const hasExplicitEndpoint = !!process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
568
|
+
if (!standardOtelEnabled && !hasExplicitEndpoint) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const initLogger = (0, pino_1.default)({
|
|
572
|
+
name: 'beamable-otlp-init',
|
|
573
|
+
level: 'info',
|
|
574
|
+
}, process.stdout);
|
|
575
|
+
initLogger.info('[OTLP] Starting collector setup in background (non-blocking)...');
|
|
576
|
+
discoverOrStartCollector(initLogger, standardOtelEnabled, env)
|
|
577
|
+
.then((result) => {
|
|
578
|
+
if (result) {
|
|
579
|
+
initLogger.info(`[OTLP] Collector setup complete in background, endpoint: ${result}`);
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
initLogger.warn('[OTLP] Collector setup returned null, OTLP logging will be disabled');
|
|
583
|
+
}
|
|
584
|
+
})
|
|
585
|
+
.catch((error) => {
|
|
586
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
587
|
+
initLogger.error(`[OTLP] Failed to setup collector in background: ${errorMsg}`);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
function setupCollectorBeforeLogging(env, timeoutMs = 60000) {
|
|
591
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
592
|
+
const useLocalOtel = !!process.env.BEAM_LOCAL_OTEL;
|
|
593
|
+
const standardOtelEnabled = (isInDocker || useLocalOtel) && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
594
|
+
const hasExplicitEndpoint = !!process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
595
|
+
if (!standardOtelEnabled && !hasExplicitEndpoint) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
const initLogger = (0, pino_1.default)({
|
|
599
|
+
name: 'beamable-otlp-init',
|
|
600
|
+
level: 'info',
|
|
601
|
+
}, process.stdout);
|
|
602
|
+
initLogger.info('[OTLP] Setting up collector before logging initialization...');
|
|
603
|
+
let endpoint = null;
|
|
604
|
+
let completed = false;
|
|
605
|
+
let setupError = null;
|
|
606
|
+
discoverOrStartCollector(initLogger, standardOtelEnabled, env)
|
|
607
|
+
.then((result) => {
|
|
608
|
+
endpoint = result;
|
|
609
|
+
completed = true;
|
|
610
|
+
if (result) {
|
|
611
|
+
initLogger.info(`[OTLP] Collector setup complete, endpoint: ${result}`);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
initLogger.warn('[OTLP] Collector setup returned null, OTLP logging will be disabled');
|
|
615
|
+
}
|
|
616
|
+
return result;
|
|
617
|
+
})
|
|
618
|
+
.catch((error) => {
|
|
619
|
+
setupError = error instanceof Error ? error.message : String(error);
|
|
620
|
+
completed = true;
|
|
621
|
+
initLogger.error(`[OTLP] Failed to setup collector: ${setupError}`);
|
|
622
|
+
endpoint = null;
|
|
623
|
+
});
|
|
624
|
+
const startTime = Date.now();
|
|
625
|
+
const timeoutId = setTimeout(() => {
|
|
626
|
+
if (!completed) {
|
|
627
|
+
initLogger.warn(`[OTLP] Collector setup timeout after ${timeoutMs}ms, continuing without OTLP`);
|
|
628
|
+
completed = true;
|
|
629
|
+
}
|
|
630
|
+
}, timeoutMs);
|
|
631
|
+
try {
|
|
632
|
+
deasync_1.default.loopWhile(() => {
|
|
633
|
+
const elapsed = Date.now() - startTime;
|
|
634
|
+
if (elapsed >= timeoutMs) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
return !completed;
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
initLogger.error(`[OTLP] Error during collector setup wait: ${error instanceof Error ? error.message : String(error)}`);
|
|
642
|
+
}
|
|
643
|
+
clearTimeout(timeoutId);
|
|
644
|
+
if (completed && endpoint) {
|
|
645
|
+
initLogger.info('[OTLP] Collector is ready, proceeding with logger creation');
|
|
646
|
+
return endpoint;
|
|
647
|
+
}
|
|
648
|
+
else if (setupError) {
|
|
649
|
+
initLogger.error(`[OTLP] Collector setup failed: ${setupError}, continuing without OTLP`);
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
initLogger.warn('[OTLP] Collector setup did not complete in time, continuing without OTLP');
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
async function startCollector(logger, otlpEndpoint, env) {
|
|
658
|
+
var _a, _b;
|
|
659
|
+
logger.info('[Collector] === startCollector() ENTERED ===');
|
|
660
|
+
globalCollectorStartError = null;
|
|
661
|
+
globalCollectorInitError = null;
|
|
662
|
+
let clickhouseEndpoint = process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
663
|
+
let clickhouseUsername = process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
664
|
+
let clickhousePassword = process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
665
|
+
if ((!clickhouseEndpoint || !clickhouseUsername || !clickhousePassword) && env) {
|
|
666
|
+
try {
|
|
667
|
+
logger.info('[Collector] Fetching ClickHouse credentials from Beamable API...');
|
|
668
|
+
const credStartTime = Date.now();
|
|
669
|
+
const credentials = await fetchClickHouseCredentials(env);
|
|
670
|
+
const credElapsed = Date.now() - credStartTime;
|
|
671
|
+
logger.info(`[Collector] ClickHouse credentials fetch completed in ${credElapsed}ms`);
|
|
672
|
+
clickhouseEndpoint = credentials.endpoint;
|
|
673
|
+
clickhouseUsername = credentials.username;
|
|
674
|
+
clickhousePassword = credentials.password;
|
|
675
|
+
addAuthEnvironmentVars(clickhouseEndpoint, clickhouseUsername, clickhousePassword);
|
|
676
|
+
const verifyEndpoint = process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
677
|
+
const verifyUsername = process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
678
|
+
const verifyPassword = process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
679
|
+
if (!verifyEndpoint || !verifyUsername || !verifyPassword) {
|
|
680
|
+
logger.error(`[Collector] CRITICAL: Credentials were set but are missing from process.env! This should never happen.`);
|
|
681
|
+
throw new Error('Failed to persist ClickHouse credentials in process.env');
|
|
682
|
+
}
|
|
683
|
+
logger.info('[Collector] ClickHouse credentials fetched from API and verified in process.env');
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
const errorMsg = `[Collector] Failed to fetch ClickHouse credentials from API: ${error instanceof Error ? error.message : String(error)}`;
|
|
687
|
+
logger.error(errorMsg);
|
|
688
|
+
throw new Error(errorMsg);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (!clickhouseEndpoint || !clickhouseUsername || !clickhousePassword) {
|
|
692
|
+
const errorMsg = `[Collector] Required ClickHouse credentials are missing. Set BEAM_CLICKHOUSE_ENDPOINT, BEAM_CLICKHOUSE_USERNAME, and BEAM_CLICKHOUSE_PASSWORD, or ensure the API endpoint is accessible.`;
|
|
693
|
+
logger.error(errorMsg);
|
|
694
|
+
throw new Error(errorMsg);
|
|
695
|
+
}
|
|
696
|
+
logger.info('[Collector] Step 2: Resolving collector binary and config...');
|
|
697
|
+
const resolveStartTime = Date.now();
|
|
698
|
+
const collectorInfo = await resolveCollector(true, logger);
|
|
699
|
+
const resolveElapsed = Date.now() - resolveStartTime;
|
|
700
|
+
logger.info(`[Collector] Collector binary/config resolution completed in ${resolveElapsed}ms`);
|
|
701
|
+
if (!collectorInfo.binaryPath) {
|
|
702
|
+
logger.error('[Collector] Binary not found and download failed');
|
|
703
|
+
throw new Error('Collector binary not found and download failed');
|
|
704
|
+
}
|
|
705
|
+
if (!collectorInfo.configPath) {
|
|
706
|
+
logger.error('[Collector] Config not found and download failed');
|
|
707
|
+
throw new Error('Collector config not found and download failed');
|
|
708
|
+
}
|
|
709
|
+
logger.info(`[Collector] Using binary: ${collectorInfo.binaryPath}`);
|
|
710
|
+
logger.info(`[Collector] Using config: ${collectorInfo.configPath}`);
|
|
711
|
+
if (!collectorInfo.binaryPath || !(0, fs_1.existsSync)(collectorInfo.binaryPath)) {
|
|
712
|
+
throw new Error(`Collector binary not found at ${collectorInfo.binaryPath}`);
|
|
713
|
+
}
|
|
714
|
+
if (process.platform !== 'win32') {
|
|
715
|
+
try {
|
|
716
|
+
const fsPromises = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
|
717
|
+
const stats = await fsPromises.stat(collectorInfo.binaryPath);
|
|
718
|
+
const mode = Number(stats.mode);
|
|
719
|
+
const isExecutable = !!(mode & parseInt('111', 8));
|
|
720
|
+
if (!isExecutable) {
|
|
721
|
+
logger.warn(`[Collector] Binary may not be executable. Mode: ${mode.toString(8)}`);
|
|
722
|
+
try {
|
|
723
|
+
(0, fs_1.chmodSync)(collectorInfo.binaryPath, 0o755);
|
|
724
|
+
logger.info(`[Collector] Made binary executable`);
|
|
725
|
+
}
|
|
726
|
+
catch (chmodError) {
|
|
727
|
+
logger.error(`[Collector] Failed to make binary executable: ${chmodError instanceof Error ? chmodError.message : String(chmodError)}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
catch (statError) {
|
|
732
|
+
logger.warn(`[Collector] Could not check binary permissions: ${statError instanceof Error ? statError.message : String(statError)}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
addCollectorConfigurationToEnvironment();
|
|
736
|
+
if (!process.env.BEAM_COLLECTOR_DISCOVERY_PORT) {
|
|
737
|
+
process.env.BEAM_COLLECTOR_DISCOVERY_PORT = String(DISCOVERY_PORT);
|
|
738
|
+
logger.info(`[Collector] Set BEAM_COLLECTOR_DISCOVERY_PORT=${DISCOVERY_PORT} (default)`);
|
|
739
|
+
}
|
|
740
|
+
let localEndpoint = otlpEndpoint;
|
|
741
|
+
if (!localEndpoint) {
|
|
742
|
+
localEndpoint = '0.0.0.0:4318';
|
|
743
|
+
}
|
|
744
|
+
localEndpoint = localEndpoint.replace(/^http:\/\//, '').replace(/^https:\/\//, '');
|
|
745
|
+
const prometheusPort = await findFreePort();
|
|
746
|
+
const collectorEnv = {
|
|
747
|
+
...process.env,
|
|
748
|
+
BEAM_OTLP_HTTP_ENDPOINT: localEndpoint,
|
|
749
|
+
BEAM_COLLECTOR_DISCOVERY_PORT: String(DISCOVERY_PORT),
|
|
750
|
+
BEAM_COLLECTOR_PROMETHEUS_PORT: String(prometheusPort),
|
|
751
|
+
};
|
|
752
|
+
const collectorProcess = (0, child_process_1.spawn)(collectorInfo.binaryPath, ['--config', collectorInfo.configPath], {
|
|
753
|
+
env: collectorEnv,
|
|
754
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
755
|
+
detached: false,
|
|
756
|
+
});
|
|
757
|
+
globalCollectorProcess = collectorProcess;
|
|
758
|
+
globalCollectorStartError = null;
|
|
759
|
+
globalCollectorExitCode = null;
|
|
760
|
+
globalCollectorStderr = [];
|
|
761
|
+
(_a = collectorProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
762
|
+
const outputLine = data.toString().trim();
|
|
763
|
+
logger.info(`[Collector OUT] ${outputLine}`);
|
|
764
|
+
});
|
|
765
|
+
(_b = collectorProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
766
|
+
const errorLine = data.toString().trim();
|
|
767
|
+
globalCollectorStderr.push(errorLine);
|
|
768
|
+
if (globalCollectorStderr.length > 50) {
|
|
769
|
+
globalCollectorStderr.shift();
|
|
770
|
+
}
|
|
771
|
+
logger.info(`[Collector ERR] ${errorLine}`);
|
|
772
|
+
});
|
|
773
|
+
collectorProcess.on('error', (err) => {
|
|
774
|
+
globalCollectorStartError = err.message;
|
|
775
|
+
logger.error(`[Collector] Failed to start: ${err.message}`);
|
|
776
|
+
});
|
|
777
|
+
collectorProcess.on('exit', (code, signal) => {
|
|
778
|
+
globalCollectorExitCode = code;
|
|
779
|
+
logger.warn(`[Collector] Process exited with code ${code}${signal ? `, signal ${signal}` : ''}`);
|
|
780
|
+
if (code !== 0 && globalCollectorStderr.length > 0) {
|
|
781
|
+
logger.error(`[Collector] Exit error - Last ${Math.min(10, globalCollectorStderr.length)} stderr lines:`);
|
|
782
|
+
globalCollectorStderr.slice(-10).forEach((line) => {
|
|
783
|
+
logger.error(`[Collector ERR] ${line}`);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
logger.info(`[Collector] Started with PID ${collectorProcess.pid}, endpoint: ${localEndpoint}`);
|
|
788
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
789
|
+
if (globalCollectorExitCode !== null && globalCollectorExitCode !== 0) {
|
|
790
|
+
const stderrMsg = globalCollectorStderr.length > 0
|
|
791
|
+
? ` Stderr: ${globalCollectorStderr.join('; ')}`
|
|
792
|
+
: ' No stderr output captured.';
|
|
793
|
+
throw new Error(`Collector process exited immediately after startup with code ${globalCollectorExitCode}.${stderrMsg}`);
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
process: collectorProcess,
|
|
797
|
+
endpoint: `http://${localEndpoint}`,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function getCollectorProcessStatus() {
|
|
801
|
+
var _a;
|
|
802
|
+
const processAlive = globalCollectorProcess !== null &&
|
|
803
|
+
globalCollectorProcess.exitCode === null &&
|
|
804
|
+
globalCollectorProcess.killed === false;
|
|
805
|
+
const stderrText = globalCollectorStderr.join('\n');
|
|
806
|
+
const isReady = stderrText.includes('Everything is ready') ||
|
|
807
|
+
stderrText.includes('Begin running and processing data');
|
|
808
|
+
const actualInitError = (processAlive && isReady) ? null : globalCollectorInitError;
|
|
809
|
+
const basePath = getCollectorStoragePath();
|
|
810
|
+
const binaryName = getCollectorBinaryName();
|
|
811
|
+
const configName = 'clickhouse-config.yaml';
|
|
812
|
+
const binaryPath = (0, path_1.join)(basePath, binaryName);
|
|
813
|
+
const configPath = (0, path_1.join)(basePath, configName);
|
|
814
|
+
const binaryExists = (0, fs_1.existsSync)(binaryPath);
|
|
815
|
+
const configExists = (0, fs_1.existsSync)(configPath);
|
|
816
|
+
const isPreInstalled = binaryExists && configExists;
|
|
817
|
+
let diagnosticInitError = actualInitError;
|
|
818
|
+
if (!isPreInstalled && actualInitError && actualInitError.includes('DNS resolution failed')) {
|
|
819
|
+
const baseDirExists = (0, fs_1.existsSync)(basePath);
|
|
820
|
+
diagnosticInitError = `${actualInitError} (Pre-installed collector not found: basePath=${basePath}, binaryExists=${binaryExists}, configExists=${configExists}, baseDirExists=${baseDirExists}. This suggests the Docker image was not built with the pre-installed collector, or the files are in a different location.)`;
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
hasProcess: globalCollectorProcess !== null,
|
|
824
|
+
pid: (_a = globalCollectorProcess === null || globalCollectorProcess === void 0 ? void 0 : globalCollectorProcess.pid) !== null && _a !== void 0 ? _a : null,
|
|
825
|
+
exitCode: globalCollectorExitCode,
|
|
826
|
+
startError: globalCollectorStartError,
|
|
827
|
+
initError: diagnosticInitError,
|
|
828
|
+
stderr: [...globalCollectorStderr],
|
|
829
|
+
isPreInstalled,
|
|
830
|
+
binaryPath: isPreInstalled ? binaryPath : null,
|
|
831
|
+
configPath: isPreInstalled ? configPath : null,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
async function discoverOrStartCollector(logger, standardOtelEnabled, env) {
|
|
835
|
+
if (!standardOtelEnabled) {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
if (globalCollectorStartupPromise) {
|
|
839
|
+
logger.info('[Collector] Collector startup already in progress, waiting for existing startup to complete...');
|
|
840
|
+
try {
|
|
841
|
+
const result = await globalCollectorStartupPromise;
|
|
842
|
+
globalCollectorStartupPromise = null;
|
|
843
|
+
return result;
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
logger.error(`[Collector] Existing startup promise failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
847
|
+
globalCollectorStartupPromise = null;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
let existingEndpoint;
|
|
851
|
+
if (globalCollectorProcess) {
|
|
852
|
+
const processAlive = globalCollectorProcess.exitCode === null &&
|
|
853
|
+
globalCollectorProcess.killed === false;
|
|
854
|
+
if (processAlive) {
|
|
855
|
+
logger.info(`[Collector] Collector process already exists (PID ${globalCollectorProcess.pid}), waiting for it to be ready...`);
|
|
856
|
+
existingEndpoint = process.env.BEAM_OTLP_HTTP_ENDPOINT ?
|
|
857
|
+
`http://${process.env.BEAM_OTLP_HTTP_ENDPOINT}` :
|
|
858
|
+
'http://0.0.0.0:4318';
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
logger.warn(`[Collector] Previous collector process (PID ${globalCollectorProcess.pid}) is dead, starting new one...`);
|
|
862
|
+
globalCollectorProcess = null;
|
|
863
|
+
globalCollectorExitCode = null;
|
|
864
|
+
globalCollectorStderr = [];
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (!globalCollectorProcess) {
|
|
868
|
+
logger.info('[Collector] Quick check for existing collector via UDP discovery (timeout: 1s, matching C#)...');
|
|
869
|
+
const udpStartTime = Date.now();
|
|
870
|
+
logger.info('[Collector] Calling isCollectorRunning()...');
|
|
871
|
+
const status = await isCollectorRunning();
|
|
872
|
+
const udpElapsed = Date.now() - udpStartTime;
|
|
873
|
+
logger.info(`[Collector] isCollectorRunning() returned after ${udpElapsed}ms: isRunning=${status.isRunning}, isReady=${status.isReady}, endpoint=${status.otlpEndpoint || 'none'}`);
|
|
874
|
+
if (status.isRunning && status.isReady && status.otlpEndpoint) {
|
|
875
|
+
logger.info(`[Collector] Found running collector at ${status.otlpEndpoint}, will reuse it`);
|
|
876
|
+
return `http://${status.otlpEndpoint}`;
|
|
877
|
+
}
|
|
878
|
+
logger.info('[Collector] No existing collector found, will start new one...');
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
logger.info('[Collector] Collector process already exists, skipping UDP discovery...');
|
|
882
|
+
}
|
|
883
|
+
logger.info('[Collector] Creating startup promise...');
|
|
884
|
+
const startupPromise = (async () => {
|
|
885
|
+
logger.info('[Collector] Startup promise executing...');
|
|
886
|
+
try {
|
|
887
|
+
globalCollectorInitError = null;
|
|
888
|
+
logger.info(`[Collector] Checking existingEndpoint: ${existingEndpoint || 'none'}`);
|
|
889
|
+
let endpoint;
|
|
890
|
+
if (existingEndpoint) {
|
|
891
|
+
endpoint = existingEndpoint;
|
|
892
|
+
logger.info(`[Collector] Waiting for existing collector to become ready at ${endpoint}...`);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
logger.info('[Collector] Starting new OpenTelemetry collector...');
|
|
896
|
+
const startCollectorTime = Date.now();
|
|
897
|
+
const startResult = await startCollector(logger, undefined, env);
|
|
898
|
+
const startCollectorElapsed = Date.now() - startCollectorTime;
|
|
899
|
+
logger.info(`[Collector] Collector process started in ${startCollectorElapsed}ms, endpoint: ${startResult.endpoint}`);
|
|
900
|
+
endpoint = startResult.endpoint;
|
|
901
|
+
logger.info('[Collector] Checking if collector process is still running...');
|
|
902
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
903
|
+
if (globalCollectorExitCode !== null && globalCollectorExitCode !== 0) {
|
|
904
|
+
const errorMsg = `Collector process exited immediately with code ${globalCollectorExitCode}. ${globalCollectorStderr.length > 0 ? `Stderr: ${globalCollectorStderr.join('; ')}` : 'No stderr output.'}`;
|
|
905
|
+
globalCollectorInitError = errorMsg;
|
|
906
|
+
logger.error(`[Collector] ${errorMsg}`);
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
logger.info('[Collector] Collector process is still running, proceeding to readiness check...');
|
|
910
|
+
}
|
|
911
|
+
const maxWaitTime = 60000;
|
|
912
|
+
const checkInterval = 500;
|
|
913
|
+
const maxChecks = Math.floor(maxWaitTime / checkInterval);
|
|
914
|
+
logger.info(`[Collector] Waiting for collector to become ready (checking every ${checkInterval}ms, max ${maxWaitTime / 1000}s)...`);
|
|
915
|
+
const readinessStartTime = Date.now();
|
|
916
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
917
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
918
|
+
const elapsed = Date.now() - readinessStartTime;
|
|
919
|
+
if (globalCollectorExitCode !== null && globalCollectorExitCode !== 0) {
|
|
920
|
+
const errorMsg = `Collector process exited during startup with code ${globalCollectorExitCode}. ${globalCollectorStderr.length > 0 ? `Stderr: ${globalCollectorStderr.join('; ')}` : 'No stderr output.'}`;
|
|
921
|
+
globalCollectorInitError = errorMsg;
|
|
922
|
+
logger.error(`[Collector] ${errorMsg} (after ${elapsed}ms)`);
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
logger.info(`[Collector] Checking collector readiness... (${(elapsed / 1000).toFixed(1)}s elapsed, attempt ${i + 1}/${maxChecks})`);
|
|
926
|
+
const newStatus = await isCollectorRunning();
|
|
927
|
+
logger.info(`[Collector] Collector status: isRunning=${newStatus.isRunning}, isReady=${newStatus.isReady}, pid=${newStatus.pid}, endpoint=${newStatus.otlpEndpoint || 'none'}`);
|
|
928
|
+
if (newStatus.isRunning && newStatus.isReady) {
|
|
929
|
+
logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint} (ready after ${elapsed}ms)`);
|
|
930
|
+
return newStatus.otlpEndpoint ? `http://${newStatus.otlpEndpoint}` : endpoint;
|
|
931
|
+
}
|
|
932
|
+
if (i > 0 && i % 2 === 0) {
|
|
933
|
+
logger.info(`[Collector] Still waiting for collector to become ready... (${(elapsed / 1000).toFixed(1)}s elapsed)`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (globalCollectorExitCode !== null && globalCollectorExitCode !== 0) {
|
|
937
|
+
const errorMsg = `Collector process exited with code ${globalCollectorExitCode}. ${globalCollectorStderr.length > 0 ? `Stderr: ${globalCollectorStderr.join('; ')}` : 'No stderr output.'}`;
|
|
938
|
+
globalCollectorInitError = errorMsg;
|
|
939
|
+
logger.error(`[Collector] ${errorMsg}`);
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
logger.error(`[Collector] Collector did not become ready within ${maxWaitTime / 1000} seconds`);
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
catch (err) {
|
|
946
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
947
|
+
globalCollectorInitError = errorMsg;
|
|
948
|
+
logger.error(`[Collector] Failed to start collector: ${errorMsg}`);
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
})();
|
|
952
|
+
globalCollectorStartupPromise = startupPromise;
|
|
953
|
+
startupPromise.finally(() => {
|
|
954
|
+
globalCollectorStartupPromise = null;
|
|
955
|
+
});
|
|
956
|
+
return await startupPromise;
|
|
957
|
+
}
|