@omen.foundation/node-microservice-runtime 0.1.35 → 0.1.37
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/collector-manager.cjs +437 -0
- package/dist/collector-manager.d.ts.map +1 -1
- package/dist/collector-manager.js +6 -4
- package/dist/collector-manager.js.map +1 -1
- 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/docs.d.ts +1 -1
- package/dist/docs.d.ts.map +1 -1
- package/dist/docs.js +2 -2
- package/dist/docs.js.map +1 -1
- package/dist/env.cjs +125 -0
- package/dist/errors.cjs +58 -0
- package/dist/federation.cjs +356 -0
- package/dist/index.cjs +49 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/inventory.cjs +361 -0
- package/dist/logger.cjs +482 -0
- package/dist/message.cjs +19 -0
- package/dist/requester.cjs +100 -0
- package/dist/routing.cjs +39 -0
- package/dist/runtime.cjs +760 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +3 -2
- package/dist/runtime.js.map +1 -1
- package/dist/services.cjs +346 -0
- package/dist/storage.cjs +147 -0
- package/dist/types.cjs +2 -0
- package/dist/utils/urls.cjs +55 -0
- package/dist/websocket.cjs +142 -0
- package/package.json +1 -1
- package/src/collector-manager.ts +6 -4
- package/src/docs.ts +2 -2
- package/src/index.ts +3 -0
- package/src/runtime.ts +3 -0
package/dist/auth.cjs
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthManager = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_url_1 = require("node:url");
|
|
6
|
+
const errors_js_1 = require("./errors.js");
|
|
7
|
+
class AuthManager {
|
|
8
|
+
constructor(env, requester) {
|
|
9
|
+
this.env = env;
|
|
10
|
+
this.requester = requester;
|
|
11
|
+
}
|
|
12
|
+
async authenticate() {
|
|
13
|
+
if (this.env.secret) {
|
|
14
|
+
await this.authenticateWithRealmSecret();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (this.env.refreshToken) {
|
|
18
|
+
await this.authenticateWithRefreshToken();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
throw new errors_js_1.AuthenticationError('Neither SECRET nor REFRESH_TOKEN is configured.');
|
|
22
|
+
}
|
|
23
|
+
async authenticateWithRealmSecret() {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
const nonce = await this.requester.request('get', 'gateway/nonce');
|
|
26
|
+
if (!(nonce === null || nonce === void 0 ? void 0 : nonce.nonce)) {
|
|
27
|
+
throw new errors_js_1.AuthenticationError('Gateway did not provide a nonce for authentication.');
|
|
28
|
+
}
|
|
29
|
+
const signature = this.calculateRealmSignature((_a = this.env.secret) !== null && _a !== void 0 ? _a : '', nonce.nonce);
|
|
30
|
+
const body = {
|
|
31
|
+
cid: this.env.cid,
|
|
32
|
+
pid: this.env.pid,
|
|
33
|
+
signature,
|
|
34
|
+
};
|
|
35
|
+
const response = await this.requester.request('post', 'gateway/auth', body);
|
|
36
|
+
if (!response || response.result !== 'ok') {
|
|
37
|
+
throw new errors_js_1.AuthenticationError(`Realm secret authentication failed with result=${(_b = response === null || response === void 0 ? void 0 : response.result) !== null && _b !== void 0 ? _b : 'unknown'}.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async authenticateWithRefreshToken() {
|
|
41
|
+
var _a;
|
|
42
|
+
const accessToken = await this.exchangeRefreshToken();
|
|
43
|
+
const body = {
|
|
44
|
+
cid: this.env.cid,
|
|
45
|
+
pid: this.env.pid,
|
|
46
|
+
token: accessToken,
|
|
47
|
+
};
|
|
48
|
+
const response = await this.requester.request('post', 'gateway/auth', body);
|
|
49
|
+
if (!response || response.result !== 'ok') {
|
|
50
|
+
throw new errors_js_1.AuthenticationError(`Refresh-token authentication failed with result=${(_a = response === null || response === void 0 ? void 0 : response.result) !== null && _a !== void 0 ? _a : 'unknown'}.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
calculateRealmSignature(secret, nonce) {
|
|
54
|
+
const hash = (0, node_crypto_1.createHash)('md5');
|
|
55
|
+
hash.update(secret + nonce, 'utf8');
|
|
56
|
+
return hash.digest('base64');
|
|
57
|
+
}
|
|
58
|
+
async exchangeRefreshToken() {
|
|
59
|
+
var _a;
|
|
60
|
+
if (!this.env.refreshToken) {
|
|
61
|
+
throw new errors_js_1.AuthenticationError('REFRESH_TOKEN missing.');
|
|
62
|
+
}
|
|
63
|
+
const baseUrl = this.hostToHttpUrl();
|
|
64
|
+
const tokenUrl = new node_url_1.URL('/basic/auth/token', baseUrl).toString();
|
|
65
|
+
const response = await fetch(tokenUrl, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/json',
|
|
69
|
+
Accept: 'application/json',
|
|
70
|
+
'beam-scope': `${this.env.cid}.${this.env.pid}`,
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
grant_type: 'refresh_token',
|
|
74
|
+
refresh_token: this.env.refreshToken,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new errors_js_1.AuthenticationError(`Failed to retrieve access token. status=${response.status}`);
|
|
79
|
+
}
|
|
80
|
+
const payload = (await response.json());
|
|
81
|
+
if (!payload.access_token) {
|
|
82
|
+
throw new errors_js_1.AuthenticationError(`Refresh-token exchange failed: ${(_a = payload.error) !== null && _a !== void 0 ? _a : 'unknown error'}`);
|
|
83
|
+
}
|
|
84
|
+
return payload.access_token;
|
|
85
|
+
}
|
|
86
|
+
hostToHttpUrl() {
|
|
87
|
+
const host = this.env.host.replace(/\/socket$/, '');
|
|
88
|
+
if (host.startsWith('wss://')) {
|
|
89
|
+
return `https://${host.substring('wss://'.length)}`;
|
|
90
|
+
}
|
|
91
|
+
if (host.startsWith('ws://')) {
|
|
92
|
+
return `http://${host.substring('ws://'.length)}`;
|
|
93
|
+
}
|
|
94
|
+
return host;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.AuthManager = AuthManager;
|
|
@@ -0,0 +1,437 @@
|
|
|
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.fetchClickHouseCredentials = fetchClickHouseCredentials;
|
|
7
|
+
exports.getClickHouseCredentialsStatus = getClickHouseCredentialsStatus;
|
|
8
|
+
exports.isCollectorRunning = isCollectorRunning;
|
|
9
|
+
exports.addAuthEnvironmentVars = addAuthEnvironmentVars;
|
|
10
|
+
exports.startCollector = startCollector;
|
|
11
|
+
exports.getCollectorProcessStatus = getCollectorProcessStatus;
|
|
12
|
+
exports.discoverOrStartCollector = discoverOrStartCollector;
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const fs_1 = require("fs");
|
|
15
|
+
const path_1 = require("path");
|
|
16
|
+
const promises_1 = require("stream/promises");
|
|
17
|
+
const zlib_1 = require("zlib");
|
|
18
|
+
const node_crypto_1 = require("node:crypto");
|
|
19
|
+
const dgram_1 = __importDefault(require("dgram"));
|
|
20
|
+
const urls_js_1 = require("./utils/urls.js");
|
|
21
|
+
const COLLECTOR_VERSION = '1.0.1';
|
|
22
|
+
const COLLECTOR_DOWNLOAD_BASE = `https://collectors.beamable.com/version/${COLLECTOR_VERSION}`;
|
|
23
|
+
const DISCOVERY_PORT = parseInt(process.env.BEAM_COLLECTOR_DISCOVERY_PORT || '8688', 10);
|
|
24
|
+
let globalCollectorProcess = null;
|
|
25
|
+
let globalCollectorStartError = null;
|
|
26
|
+
let globalCollectorExitCode = null;
|
|
27
|
+
let globalCollectorStderr = [];
|
|
28
|
+
let globalCollectorInitError = null;
|
|
29
|
+
const DISCOVERY_DELAY = 100;
|
|
30
|
+
const DISCOVERY_ATTEMPTS = 10;
|
|
31
|
+
function calculateSignature(pid, secret, uriPathAndQuery, body = null, version = '1') {
|
|
32
|
+
let dataToSign = `${secret}${pid}${version}${uriPathAndQuery}`;
|
|
33
|
+
if (body) {
|
|
34
|
+
dataToSign += body;
|
|
35
|
+
}
|
|
36
|
+
const hash = (0, node_crypto_1.createHash)('md5').update(dataToSign, 'utf8').digest('base64');
|
|
37
|
+
return hash;
|
|
38
|
+
}
|
|
39
|
+
function getPathAndQuery(url) {
|
|
40
|
+
try {
|
|
41
|
+
const urlObj = new URL(url);
|
|
42
|
+
return urlObj.pathname + urlObj.search;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function fetchClickHouseCredentials(env) {
|
|
49
|
+
const apiUrl = (0, urls_js_1.hostToHttpUrl)(env.host);
|
|
50
|
+
const uriPath = '/api/beamo/otel/auth/writer/config';
|
|
51
|
+
const configUrl = new URL(uriPath, apiUrl).toString();
|
|
52
|
+
const secret = process.env.SECRET;
|
|
53
|
+
if (!secret) {
|
|
54
|
+
throw new Error('SECRET environment variable is required to fetch ClickHouse credentials');
|
|
55
|
+
}
|
|
56
|
+
const pathAndQuery = getPathAndQuery(uriPath);
|
|
57
|
+
const signature = calculateSignature(env.pid, secret, pathAndQuery, null, '1');
|
|
58
|
+
const headers = {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
Accept: 'application/json',
|
|
61
|
+
'X-BEAM-SCOPE': `${env.cid}.${env.pid}`,
|
|
62
|
+
'X-BEAM-SIGNATURE': signature,
|
|
63
|
+
};
|
|
64
|
+
const response = await fetch(configUrl, {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
headers,
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const errorText = await response.text().catch(() => 'Unknown error');
|
|
70
|
+
throw new Error(`Failed to fetch ClickHouse credentials from ${configUrl}: ${response.status} ${response.statusText} - ${errorText.substring(0, 200)}`);
|
|
71
|
+
}
|
|
72
|
+
const credentials = await response.json();
|
|
73
|
+
if (!credentials.endpoint || !credentials.username || !credentials.password) {
|
|
74
|
+
throw new Error('Invalid ClickHouse credentials response: missing required fields');
|
|
75
|
+
}
|
|
76
|
+
return credentials;
|
|
77
|
+
}
|
|
78
|
+
function getCollectorStoragePath() {
|
|
79
|
+
const tempDir = process.env.TMPDIR || process.env.TMP || '/tmp';
|
|
80
|
+
return (0, path_1.join)(tempDir, 'beam', 'collectors', COLLECTOR_VERSION);
|
|
81
|
+
}
|
|
82
|
+
function getCollectorBinaryName() {
|
|
83
|
+
const platform = process.platform;
|
|
84
|
+
const arch = process.arch;
|
|
85
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
86
|
+
return 'collector-linux-amd64';
|
|
87
|
+
}
|
|
88
|
+
else if (platform === 'linux' && arch === 'arm64') {
|
|
89
|
+
return 'collector-linux-arm64';
|
|
90
|
+
}
|
|
91
|
+
else if (platform === 'darwin' && arch === 'x64') {
|
|
92
|
+
return 'collector-darwin-amd64';
|
|
93
|
+
}
|
|
94
|
+
else if (platform === 'darwin' && arch === 'arm64') {
|
|
95
|
+
return 'collector-darwin-arm64';
|
|
96
|
+
}
|
|
97
|
+
else if (platform === 'win32' && arch === 'x64') {
|
|
98
|
+
return 'collector-windows-amd64.exe';
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
101
|
+
}
|
|
102
|
+
async function downloadAndDecompressGzip(url, outputPath, makeExecutable = false) {
|
|
103
|
+
let response;
|
|
104
|
+
try {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
107
|
+
try {
|
|
108
|
+
response = await fetch(url, {
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
headers: {
|
|
111
|
+
'User-Agent': 'Beamable-Node-Microservice-Runtime/1.0',
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
clearTimeout(timeoutId);
|
|
115
|
+
}
|
|
116
|
+
catch (fetchError) {
|
|
117
|
+
clearTimeout(timeoutId);
|
|
118
|
+
if (fetchError instanceof Error) {
|
|
119
|
+
if (fetchError.name === 'AbortError') {
|
|
120
|
+
throw new Error(`Timeout downloading ${url} (30s limit exceeded)`);
|
|
121
|
+
}
|
|
122
|
+
const errorMsg = fetchError.message.toLowerCase();
|
|
123
|
+
if (errorMsg.includes('enotfound') || errorMsg.includes('dns')) {
|
|
124
|
+
throw new Error(`DNS resolution failed for ${url}. Check network connectivity and DNS settings.`);
|
|
125
|
+
}
|
|
126
|
+
if (errorMsg.includes('econnrefused') || errorMsg.includes('connection refused')) {
|
|
127
|
+
throw new Error(`Connection refused to ${url}. Server may be down or firewall blocking.`);
|
|
128
|
+
}
|
|
129
|
+
if (errorMsg.includes('etimedout') || errorMsg.includes('timeout')) {
|
|
130
|
+
throw new Error(`Connection timeout to ${url}. Network may be slow or unreachable.`);
|
|
131
|
+
}
|
|
132
|
+
if (errorMsg.includes('certificate') || errorMsg.includes('ssl') || errorMsg.includes('tls')) {
|
|
133
|
+
throw new Error(`SSL/TLS error connecting to ${url}: ${fetchError.message}`);
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`Network error downloading ${url}: ${fetchError.name} - ${fetchError.message}`);
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`Network error downloading ${url}: ${String(fetchError)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof Error) {
|
|
142
|
+
if (error.message.includes('Timeout') || error.message.includes('DNS') || error.message.includes('Connection')) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Network error downloading ${url}: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Network error downloading ${url}: ${String(error)}`);
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
const errorText = await response.text().catch(() => '');
|
|
151
|
+
throw new Error(`HTTP ${response.status} downloading ${url}: ${response.statusText}${errorText ? ` - ${errorText.substring(0, 200)}` : ''}`);
|
|
152
|
+
}
|
|
153
|
+
const dir = (0, path_1.dirname)(outputPath);
|
|
154
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
155
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
const gunzip = (0, zlib_1.createGunzip)();
|
|
158
|
+
const writeStream = (0, fs_1.createWriteStream)(outputPath);
|
|
159
|
+
await (0, promises_1.pipeline)(response.body, gunzip, writeStream);
|
|
160
|
+
if (makeExecutable && process.platform !== 'win32') {
|
|
161
|
+
try {
|
|
162
|
+
(0, fs_1.chmodSync)(outputPath, 0o755);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error(`Failed to make ${outputPath} executable:`, error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function resolveCollector(allowDownload = true, logger) {
|
|
170
|
+
const basePath = getCollectorStoragePath();
|
|
171
|
+
const binaryName = getCollectorBinaryName();
|
|
172
|
+
const configName = 'clickhouse-config.yaml';
|
|
173
|
+
const binaryPath = (0, path_1.join)(basePath, binaryName);
|
|
174
|
+
const configPath = (0, path_1.join)(basePath, configName);
|
|
175
|
+
const itemsToDownload = [];
|
|
176
|
+
if (!(0, fs_1.existsSync)(binaryPath) && allowDownload) {
|
|
177
|
+
const binaryUrl = `${COLLECTOR_DOWNLOAD_BASE}/${binaryName}.gz`;
|
|
178
|
+
itemsToDownload.push({ url: binaryUrl, path: binaryPath, executable: true });
|
|
179
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Will download binary from: ${binaryUrl}`);
|
|
180
|
+
}
|
|
181
|
+
else if ((0, fs_1.existsSync)(binaryPath)) {
|
|
182
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Binary found at: ${binaryPath}`);
|
|
183
|
+
}
|
|
184
|
+
if (!(0, fs_1.existsSync)(configPath) && allowDownload) {
|
|
185
|
+
const configUrl = `${COLLECTOR_DOWNLOAD_BASE}/${configName}.gz`;
|
|
186
|
+
itemsToDownload.push({ url: configUrl, path: configPath, executable: false });
|
|
187
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Will download config from: ${configUrl}`);
|
|
188
|
+
}
|
|
189
|
+
else if ((0, fs_1.existsSync)(configPath)) {
|
|
190
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Config found at: ${configPath}`);
|
|
191
|
+
}
|
|
192
|
+
for (const item of itemsToDownload) {
|
|
193
|
+
try {
|
|
194
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Downloading ${item.url}...`);
|
|
195
|
+
await downloadAndDecompressGzip(item.url, item.path, item.executable);
|
|
196
|
+
logger === null || logger === void 0 ? void 0 : logger.info(`[Collector] Downloaded to ${item.path}`);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
200
|
+
logger === null || logger === void 0 ? void 0 : logger.error(`[Collector] Failed to download ${item.url}: ${errorMsg}`);
|
|
201
|
+
throw new Error(`Failed to download collector ${item.executable ? 'binary' : 'config'} from ${item.url}: ${errorMsg}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
binaryPath: (0, fs_1.existsSync)(binaryPath) ? binaryPath : null,
|
|
206
|
+
configPath: (0, fs_1.existsSync)(configPath) ? configPath : null,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function discoverCollectorViaUDP(timeoutMs = 5000) {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
const socket = dgram_1.default.createSocket('udp4');
|
|
212
|
+
const discovered = [];
|
|
213
|
+
let timeout;
|
|
214
|
+
socket.on('message', (msg) => {
|
|
215
|
+
try {
|
|
216
|
+
const message = JSON.parse(msg.toString());
|
|
217
|
+
if (message.version === COLLECTOR_VERSION && message.status === 'READY') {
|
|
218
|
+
discovered.push(message);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
socket.on('error', () => {
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
socket.close();
|
|
227
|
+
resolve(null);
|
|
228
|
+
});
|
|
229
|
+
socket.bind(() => {
|
|
230
|
+
socket.setBroadcast(true);
|
|
231
|
+
timeout = setTimeout(() => {
|
|
232
|
+
socket.close();
|
|
233
|
+
resolve(discovered.length > 0 ? discovered[0] : null);
|
|
234
|
+
}, timeoutMs);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function getClickHouseCredentialsStatus() {
|
|
239
|
+
const hasEndpoint = !!process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
240
|
+
const hasUsername = !!process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
241
|
+
const hasPassword = !!process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
242
|
+
if (hasEndpoint && hasUsername && hasPassword) {
|
|
243
|
+
return {
|
|
244
|
+
hasEndpoint: true,
|
|
245
|
+
hasUsername: true,
|
|
246
|
+
hasPassword: true,
|
|
247
|
+
source: 'environment',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
hasEndpoint,
|
|
252
|
+
hasUsername,
|
|
253
|
+
hasPassword,
|
|
254
|
+
source: 'missing',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function isCollectorRunning() {
|
|
258
|
+
try {
|
|
259
|
+
const discovered = await discoverCollectorViaUDP(2000);
|
|
260
|
+
if (discovered) {
|
|
261
|
+
return {
|
|
262
|
+
isRunning: true,
|
|
263
|
+
isReady: discovered.status === 'READY',
|
|
264
|
+
pid: discovered.pid,
|
|
265
|
+
otlpEndpoint: discovered.otlpEndpoint,
|
|
266
|
+
version: discovered.version,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
isRunning: false,
|
|
274
|
+
isReady: false,
|
|
275
|
+
pid: 0,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function addCollectorConfigurationToEnvironment() {
|
|
279
|
+
const defaults = {
|
|
280
|
+
BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT: '5s',
|
|
281
|
+
BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE: '5000',
|
|
282
|
+
BEAM_CLICKHOUSE_EXPORTER_TIMEOUT: '5s',
|
|
283
|
+
BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE: '1000',
|
|
284
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED: 'true',
|
|
285
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL: '5s',
|
|
286
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL: '30s',
|
|
287
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME: '300s',
|
|
288
|
+
};
|
|
289
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
290
|
+
if (!process.env[key]) {
|
|
291
|
+
process.env[key] = defaultValue;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function addAuthEnvironmentVars(endpoint, username, password) {
|
|
296
|
+
process.env.BEAM_CLICKHOUSE_ENDPOINT = endpoint;
|
|
297
|
+
process.env.BEAM_CLICKHOUSE_USERNAME = username;
|
|
298
|
+
process.env.BEAM_CLICKHOUSE_PASSWORD = password;
|
|
299
|
+
if (!process.env.BEAM_CLICKHOUSE_ENDPOINT || !process.env.BEAM_CLICKHOUSE_USERNAME || !process.env.BEAM_CLICKHOUSE_PASSWORD) {
|
|
300
|
+
throw new Error(`Failed to set ClickHouse credentials in process.env - this should never happen in Node.js`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async function startCollector(logger, otlpEndpoint, env) {
|
|
304
|
+
var _a, _b;
|
|
305
|
+
globalCollectorStartError = null;
|
|
306
|
+
globalCollectorInitError = null;
|
|
307
|
+
let clickhouseEndpoint = process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
308
|
+
let clickhouseUsername = process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
309
|
+
let clickhousePassword = process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
310
|
+
if ((!clickhouseEndpoint || !clickhouseUsername || !clickhousePassword) && env) {
|
|
311
|
+
try {
|
|
312
|
+
logger.info('[Collector] Fetching ClickHouse credentials from Beamable API...');
|
|
313
|
+
const credentials = await fetchClickHouseCredentials(env);
|
|
314
|
+
clickhouseEndpoint = credentials.endpoint;
|
|
315
|
+
clickhouseUsername = credentials.username;
|
|
316
|
+
clickhousePassword = credentials.password;
|
|
317
|
+
addAuthEnvironmentVars(clickhouseEndpoint, clickhouseUsername, clickhousePassword);
|
|
318
|
+
const verifyEndpoint = process.env.BEAM_CLICKHOUSE_ENDPOINT;
|
|
319
|
+
const verifyUsername = process.env.BEAM_CLICKHOUSE_USERNAME;
|
|
320
|
+
const verifyPassword = process.env.BEAM_CLICKHOUSE_PASSWORD;
|
|
321
|
+
if (!verifyEndpoint || !verifyUsername || !verifyPassword) {
|
|
322
|
+
logger.error(`[Collector] CRITICAL: Credentials were set but are missing from process.env! This should never happen.`);
|
|
323
|
+
throw new Error('Failed to persist ClickHouse credentials in process.env');
|
|
324
|
+
}
|
|
325
|
+
logger.info('[Collector] ClickHouse credentials fetched from API and verified in process.env');
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
const errorMsg = `[Collector] Failed to fetch ClickHouse credentials from API: ${error instanceof Error ? error.message : String(error)}`;
|
|
329
|
+
logger.error(errorMsg);
|
|
330
|
+
throw new Error(errorMsg);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!clickhouseEndpoint || !clickhouseUsername || !clickhousePassword) {
|
|
334
|
+
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.`;
|
|
335
|
+
logger.error(errorMsg);
|
|
336
|
+
throw new Error(errorMsg);
|
|
337
|
+
}
|
|
338
|
+
logger.info('[Collector] Resolving collector binary and config...');
|
|
339
|
+
const collectorInfo = await resolveCollector(true, logger);
|
|
340
|
+
if (!collectorInfo.binaryPath) {
|
|
341
|
+
logger.error('[Collector] Binary not found and download failed');
|
|
342
|
+
throw new Error('Collector binary not found and download failed');
|
|
343
|
+
}
|
|
344
|
+
if (!collectorInfo.configPath) {
|
|
345
|
+
logger.error('[Collector] Config not found and download failed');
|
|
346
|
+
throw new Error('Collector config not found and download failed');
|
|
347
|
+
}
|
|
348
|
+
logger.info(`[Collector] Using binary: ${collectorInfo.binaryPath}`);
|
|
349
|
+
logger.info(`[Collector] Using config: ${collectorInfo.configPath}`);
|
|
350
|
+
addCollectorConfigurationToEnvironment();
|
|
351
|
+
let localEndpoint = otlpEndpoint;
|
|
352
|
+
if (!localEndpoint) {
|
|
353
|
+
localEndpoint = '0.0.0.0:4318';
|
|
354
|
+
}
|
|
355
|
+
localEndpoint = localEndpoint.replace(/^http:\/\//, '').replace(/^https:\/\//, '');
|
|
356
|
+
const collectorEnv = {
|
|
357
|
+
...process.env,
|
|
358
|
+
BEAM_OTLP_HTTP_ENDPOINT: localEndpoint,
|
|
359
|
+
BEAM_COLLECTOR_DISCOVERY_PORT: String(DISCOVERY_PORT),
|
|
360
|
+
};
|
|
361
|
+
const collectorProcess = (0, child_process_1.spawn)(collectorInfo.binaryPath, ['--config', collectorInfo.configPath], {
|
|
362
|
+
env: collectorEnv,
|
|
363
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
364
|
+
detached: false,
|
|
365
|
+
});
|
|
366
|
+
globalCollectorProcess = collectorProcess;
|
|
367
|
+
globalCollectorStartError = null;
|
|
368
|
+
globalCollectorExitCode = null;
|
|
369
|
+
globalCollectorStderr = [];
|
|
370
|
+
(_a = collectorProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
371
|
+
logger.debug(`[Collector] ${data.toString().trim()}`);
|
|
372
|
+
});
|
|
373
|
+
(_b = collectorProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
374
|
+
const errorLine = data.toString().trim();
|
|
375
|
+
globalCollectorStderr.push(errorLine);
|
|
376
|
+
if (globalCollectorStderr.length > 50) {
|
|
377
|
+
globalCollectorStderr.shift();
|
|
378
|
+
}
|
|
379
|
+
logger.debug(`[Collector ERR] ${errorLine}`);
|
|
380
|
+
});
|
|
381
|
+
collectorProcess.on('error', (err) => {
|
|
382
|
+
globalCollectorStartError = err.message;
|
|
383
|
+
logger.error(`[Collector] Failed to start: ${err.message}`);
|
|
384
|
+
});
|
|
385
|
+
collectorProcess.on('exit', (code) => {
|
|
386
|
+
globalCollectorExitCode = code;
|
|
387
|
+
globalCollectorProcess = null;
|
|
388
|
+
logger.warn(`[Collector] Process exited with code ${code}`);
|
|
389
|
+
});
|
|
390
|
+
logger.info(`[Collector] Started with PID ${collectorProcess.pid}, endpoint: ${localEndpoint}`);
|
|
391
|
+
return {
|
|
392
|
+
process: collectorProcess,
|
|
393
|
+
endpoint: `http://${localEndpoint}`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function getCollectorProcessStatus() {
|
|
397
|
+
var _a;
|
|
398
|
+
return {
|
|
399
|
+
hasProcess: globalCollectorProcess !== null,
|
|
400
|
+
pid: (_a = globalCollectorProcess === null || globalCollectorProcess === void 0 ? void 0 : globalCollectorProcess.pid) !== null && _a !== void 0 ? _a : null,
|
|
401
|
+
exitCode: globalCollectorExitCode,
|
|
402
|
+
startError: globalCollectorStartError,
|
|
403
|
+
initError: globalCollectorInitError,
|
|
404
|
+
stderr: [...globalCollectorStderr],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
async function discoverOrStartCollector(logger, standardOtelEnabled, env) {
|
|
408
|
+
if (!standardOtelEnabled) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const status = await isCollectorRunning();
|
|
412
|
+
if (status.isRunning && status.isReady && status.otlpEndpoint) {
|
|
413
|
+
logger.info(`[Collector] Found running collector at ${status.otlpEndpoint}`);
|
|
414
|
+
return `http://${status.otlpEndpoint}`;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
globalCollectorInitError = null;
|
|
418
|
+
logger.info('[Collector] Starting OpenTelemetry collector...');
|
|
419
|
+
const { endpoint } = await startCollector(logger, undefined, env);
|
|
420
|
+
for (let i = 0; i < DISCOVERY_ATTEMPTS; i++) {
|
|
421
|
+
await new Promise(resolve => setTimeout(resolve, DISCOVERY_DELAY));
|
|
422
|
+
const newStatus = await isCollectorRunning();
|
|
423
|
+
if (newStatus.isRunning && newStatus.isReady) {
|
|
424
|
+
logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint}`);
|
|
425
|
+
return newStatus.otlpEndpoint ? `http://${newStatus.otlpEndpoint}` : endpoint;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
logger.warn('[Collector] Collector started but not yet ready, using configured endpoint');
|
|
429
|
+
return endpoint;
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
433
|
+
globalCollectorInitError = errorMsg;
|
|
434
|
+
logger.error(`[Collector] Failed to start collector: ${errorMsg}`);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -1 +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;AAMpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAYpD,MAAM,WAAW,eAAe;IAC9B,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;AAeD;;GAEG;AACH,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAqCD;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,qBAAqB,CAAC,CAwChC;
|
|
1
|
+
{"version":3,"file":"collector-manager.d.ts","sourceRoot":"","sources":["../src/collector-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,YAAY,EAAE,MAAM,eAAe,CAAC;AAMpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAYpD,MAAM,WAAW,eAAe;IAC9B,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;AAeD;;GAEG;AACH,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAqCD;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,qBAAqB,CAAC,CAwChC;AAuMD;;GAEG;AACH,wBAAgB,8BAA8B,IAAI;IAChD,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,aAAa,GAAG,KAAK,GAAG,SAAS,CAAC;CAC3C,CAoBA;AAED;;GAEG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,eAAe,CAAC,CAqBnE;AA0BD;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYjG;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,EACrB,GAAG,CAAC,EAAE,iBAAiB,GACtB,OAAO,CAAC;IAAE,OAAO,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA2ItD;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI;IAC3C,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CASA;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,mBAAmB,EAAE,OAAO,EAC5B,GAAG,CAAC,EAAE,iBAAiB,GACtB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwCxB"}
|
|
@@ -161,11 +161,13 @@ async function downloadAndDecompressGzip(url, outputPath, makeExecutable = false
|
|
|
161
161
|
}
|
|
162
162
|
catch (error) {
|
|
163
163
|
// Re-throw with more context
|
|
164
|
-
if (error instanceof Error
|
|
165
|
-
|
|
164
|
+
if (error instanceof Error) {
|
|
165
|
+
if (error.message.includes('Timeout') || error.message.includes('DNS') || error.message.includes('Connection')) {
|
|
166
|
+
throw error; // Already has good context
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Network error downloading ${url}: ${error.message}`);
|
|
166
169
|
}
|
|
167
|
-
|
|
168
|
-
throw new Error(`Network error downloading ${url}: ${errorMsg}`);
|
|
170
|
+
throw new Error(`Network error downloading ${url}: ${String(error)}`);
|
|
169
171
|
}
|
|
170
172
|
if (!response.ok) {
|
|
171
173
|
const errorText = await response.text().catch(() => '');
|