@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,299 @@
|
|
|
1
|
+
import { spawn, ChildProcess } 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 type { Logger } from 'pino';
|
|
7
|
+
import dgram from 'dgram';
|
|
8
|
+
|
|
9
|
+
interface CollectorDiscoveryEntry {
|
|
10
|
+
version: string;
|
|
11
|
+
status: string;
|
|
12
|
+
pid: number;
|
|
13
|
+
otlpEndpoint: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CollectorStatus {
|
|
17
|
+
isRunning: boolean;
|
|
18
|
+
isReady: boolean;
|
|
19
|
+
pid: number;
|
|
20
|
+
otlpEndpoint?: string;
|
|
21
|
+
version?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const COLLECTOR_VERSION = '0.0.123'; // Match C# collector version
|
|
25
|
+
const COLLECTOR_DOWNLOAD_BASE = `https://collectors.beamable.com/version/${COLLECTOR_VERSION}`;
|
|
26
|
+
const DISCOVERY_PORT = parseInt(process.env.BEAM_COLLECTOR_DISCOVERY_PORT || '8688', 10);
|
|
27
|
+
const DISCOVERY_DELAY = 100; // milliseconds
|
|
28
|
+
const DISCOVERY_ATTEMPTS = 10;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets the collector storage directory (similar to C# LocalApplicationData/beam/collectors/version)
|
|
32
|
+
*/
|
|
33
|
+
function getCollectorStoragePath(): string {
|
|
34
|
+
// Use temp directory for now - in containers this should be writable
|
|
35
|
+
const tempDir = process.env.TMPDIR || process.env.TMP || '/tmp';
|
|
36
|
+
return join(tempDir, 'beam', 'collectors', COLLECTOR_VERSION);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets the collector binary name for the current platform
|
|
41
|
+
*/
|
|
42
|
+
function getCollectorBinaryName(): string {
|
|
43
|
+
const platform = process.platform;
|
|
44
|
+
const arch = process.arch;
|
|
45
|
+
|
|
46
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
47
|
+
return 'beamable-collector-linux-amd64';
|
|
48
|
+
} else if (platform === 'linux' && arch === 'arm64') {
|
|
49
|
+
return 'beamable-collector-linux-arm64';
|
|
50
|
+
} else if (platform === 'darwin' && arch === 'x64') {
|
|
51
|
+
return 'beamable-collector-darwin-amd64';
|
|
52
|
+
} else if (platform === 'darwin' && arch === 'arm64') {
|
|
53
|
+
return 'beamable-collector-darwin-arm64';
|
|
54
|
+
} else if (platform === 'win32' && arch === 'x64') {
|
|
55
|
+
return 'beamable-collector-windows-amd64.exe';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw new Error(`Unsupported platform: ${platform} ${arch}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Downloads and decompresses a gzipped file
|
|
63
|
+
*/
|
|
64
|
+
async function downloadAndDecompressGzip(url: string, outputPath: string, makeExecutable: boolean = false): Promise<void> {
|
|
65
|
+
const response = await fetch(url);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const dir = require('path').dirname(outputPath);
|
|
71
|
+
if (!existsSync(dir)) {
|
|
72
|
+
mkdirSync(dir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const gunzip = createGunzip();
|
|
76
|
+
const writeStream = createWriteStream(outputPath);
|
|
77
|
+
|
|
78
|
+
await pipeline(response.body as any, gunzip, writeStream);
|
|
79
|
+
|
|
80
|
+
if (makeExecutable && process.platform !== 'win32') {
|
|
81
|
+
try {
|
|
82
|
+
chmodSync(outputPath, 0o755);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`Failed to make ${outputPath} executable:`, error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the collector binary and config, downloading if needed
|
|
91
|
+
*/
|
|
92
|
+
async function resolveCollector(allowDownload: boolean = true): Promise<{ binaryPath: string | null; configPath: string | null }> {
|
|
93
|
+
const basePath = getCollectorStoragePath();
|
|
94
|
+
const binaryName = getCollectorBinaryName();
|
|
95
|
+
const configName = 'clickhouse-config.yaml';
|
|
96
|
+
|
|
97
|
+
const binaryPath = join(basePath, binaryName);
|
|
98
|
+
const configPath = join(basePath, configName);
|
|
99
|
+
|
|
100
|
+
const itemsToDownload: Array<{ url: string; path: string; executable: boolean }> = [];
|
|
101
|
+
|
|
102
|
+
if (!existsSync(binaryPath) && allowDownload) {
|
|
103
|
+
const binaryUrl = `${COLLECTOR_DOWNLOAD_BASE}/${binaryName}.gz`;
|
|
104
|
+
itemsToDownload.push({ url: binaryUrl, path: binaryPath, executable: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!existsSync(configPath) && allowDownload) {
|
|
108
|
+
const configUrl = `${COLLECTOR_DOWNLOAD_BASE}/${configName}.gz`;
|
|
109
|
+
itemsToDownload.push({ url: configUrl, path: configPath, executable: false });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const item of itemsToDownload) {
|
|
113
|
+
await downloadAndDecompressGzip(item.url, item.path, item.executable);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
binaryPath: existsSync(binaryPath) ? binaryPath : null,
|
|
118
|
+
configPath: existsSync(configPath) ? configPath : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Discovers collector via UDP broadcast
|
|
124
|
+
*/
|
|
125
|
+
function discoverCollectorViaUDP(timeoutMs: number = 5000): Promise<CollectorDiscoveryEntry | null> {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const socket = dgram.createSocket('udp4');
|
|
128
|
+
const discovered: CollectorDiscoveryEntry[] = [];
|
|
129
|
+
let timeout: NodeJS.Timeout;
|
|
130
|
+
|
|
131
|
+
socket.on('message', (msg) => {
|
|
132
|
+
try {
|
|
133
|
+
const message = JSON.parse(msg.toString()) as CollectorDiscoveryEntry;
|
|
134
|
+
// Check if version matches
|
|
135
|
+
if (message.version === COLLECTOR_VERSION && message.status === 'READY') {
|
|
136
|
+
discovered.push(message);
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// Ignore parse errors
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
socket.on('error', () => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
socket.close();
|
|
146
|
+
resolve(null);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
socket.bind(() => {
|
|
150
|
+
socket.setBroadcast(true);
|
|
151
|
+
|
|
152
|
+
timeout = setTimeout(() => {
|
|
153
|
+
socket.close();
|
|
154
|
+
// Return the first discovered collector
|
|
155
|
+
resolve(discovered.length > 0 ? discovered[0] : null);
|
|
156
|
+
}, timeoutMs);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Checks if collector is already running via UDP discovery
|
|
163
|
+
*/
|
|
164
|
+
export async function isCollectorRunning(): Promise<CollectorStatus> {
|
|
165
|
+
try {
|
|
166
|
+
const discovered = await discoverCollectorViaUDP(2000);
|
|
167
|
+
if (discovered) {
|
|
168
|
+
return {
|
|
169
|
+
isRunning: true,
|
|
170
|
+
isReady: discovered.status === 'READY',
|
|
171
|
+
pid: discovered.pid,
|
|
172
|
+
otlpEndpoint: discovered.otlpEndpoint,
|
|
173
|
+
version: discovered.version,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
// Discovery failed, collector probably not running
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
isRunning: false,
|
|
182
|
+
isReady: false,
|
|
183
|
+
pid: 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Starts the OpenTelemetry collector process
|
|
189
|
+
*/
|
|
190
|
+
export async function startCollector(
|
|
191
|
+
logger: Logger,
|
|
192
|
+
otlpEndpoint?: string
|
|
193
|
+
): Promise<{ process: ChildProcess; endpoint: string }> {
|
|
194
|
+
const collectorInfo = await resolveCollector(true);
|
|
195
|
+
|
|
196
|
+
if (!collectorInfo.binaryPath) {
|
|
197
|
+
throw new Error('Collector binary not found and download failed');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!collectorInfo.configPath) {
|
|
201
|
+
throw new Error('Collector config not found and download failed');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Determine OTLP endpoint
|
|
205
|
+
let localEndpoint = otlpEndpoint || 'localhost:4318';
|
|
206
|
+
localEndpoint = localEndpoint.replace(/^http:\/\//, '').replace(/^https:\/\//, '');
|
|
207
|
+
|
|
208
|
+
// Set environment variables for collector
|
|
209
|
+
const env = {
|
|
210
|
+
...process.env,
|
|
211
|
+
BEAM_OTLP_HTTP_ENDPOINT: localEndpoint,
|
|
212
|
+
BEAM_CLICKHOUSE_ENDPOINT: process.env.BEAM_CLICKHOUSE_ENDPOINT || '',
|
|
213
|
+
BEAM_CLICKHOUSE_USERNAME: process.env.BEAM_CLICKHOUSE_USERNAME || '',
|
|
214
|
+
BEAM_CLICKHOUSE_PASSWORD: process.env.BEAM_CLICKHOUSE_PASSWORD || '',
|
|
215
|
+
BEAM_COLLECTOR_DISCOVERY_PORT: String(DISCOVERY_PORT),
|
|
216
|
+
BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT: process.env.BEAM_CLICKHOUSE_PROCESSOR_TIMEOUT || '5s',
|
|
217
|
+
BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE: process.env.BEAM_CLICKHOUSE_PROCESSOR_BATCH_SIZE || '5000',
|
|
218
|
+
BEAM_CLICKHOUSE_EXPORTER_TIMEOUT: process.env.BEAM_CLICKHOUSE_EXPORTER_TIMEOUT || '5s',
|
|
219
|
+
BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE: process.env.BEAM_CLICKHOUSE_EXPORTER_QUEUE_SIZE || '1000',
|
|
220
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_ENABLED || 'true',
|
|
221
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_INITIAL_INTERVAL || '5s',
|
|
222
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_INTERVAL || '30s',
|
|
223
|
+
BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME: process.env.BEAM_CLICKHOUSE_EXPORTER_RETRY_MAX_ELAPSED_TIME || '300s',
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Start collector process
|
|
227
|
+
const collectorProcess = spawn(collectorInfo.binaryPath, ['--config', collectorInfo.configPath], {
|
|
228
|
+
env,
|
|
229
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
230
|
+
detached: false,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
collectorProcess.stdout?.on('data', (data) => {
|
|
234
|
+
logger.debug(`[Collector] ${data.toString().trim()}`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
collectorProcess.stderr?.on('data', (data) => {
|
|
238
|
+
logger.debug(`[Collector ERR] ${data.toString().trim()}`);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
collectorProcess.on('error', (err) => {
|
|
242
|
+
logger.error(`[Collector] Failed to start: ${err.message}`);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
collectorProcess.on('exit', (code) => {
|
|
246
|
+
logger.warn(`[Collector] Process exited with code ${code}`);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
logger.info(`[Collector] Started with PID ${collectorProcess.pid}, endpoint: ${localEndpoint}`);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
process: collectorProcess,
|
|
253
|
+
endpoint: `http://${localEndpoint}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Discovers or starts the collector and returns the OTLP endpoint
|
|
259
|
+
*/
|
|
260
|
+
export async function discoverOrStartCollector(
|
|
261
|
+
logger: Logger,
|
|
262
|
+
standardOtelEnabled: boolean
|
|
263
|
+
): Promise<string | null> {
|
|
264
|
+
if (!standardOtelEnabled) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// First, check if collector is already running
|
|
269
|
+
const status = await isCollectorRunning();
|
|
270
|
+
if (status.isRunning && status.isReady && status.otlpEndpoint) {
|
|
271
|
+
logger.info(`[Collector] Found running collector at ${status.otlpEndpoint}`);
|
|
272
|
+
return `http://${status.otlpEndpoint}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Collector not running - start it
|
|
276
|
+
try {
|
|
277
|
+
logger.info('[Collector] Starting OpenTelemetry collector...');
|
|
278
|
+
const { endpoint } = await startCollector(logger);
|
|
279
|
+
|
|
280
|
+
// Wait a bit for collector to start and become ready
|
|
281
|
+
// Try to discover it
|
|
282
|
+
for (let i = 0; i < DISCOVERY_ATTEMPTS; i++) {
|
|
283
|
+
await new Promise(resolve => setTimeout(resolve, DISCOVERY_DELAY));
|
|
284
|
+
const newStatus = await isCollectorRunning();
|
|
285
|
+
if (newStatus.isRunning && newStatus.isReady) {
|
|
286
|
+
logger.info(`[Collector] Collector is ready at ${newStatus.otlpEndpoint || endpoint}`);
|
|
287
|
+
return newStatus.otlpEndpoint ? `http://${newStatus.otlpEndpoint}` : endpoint;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Return the endpoint we started with, even if discovery didn't find it yet
|
|
292
|
+
logger.warn('[Collector] Collector started but not yet ready, using configured endpoint');
|
|
293
|
+
return endpoint;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logger.error(`[Collector] Failed to start collector: ${err instanceof Error ? err.message : String(err)}`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
package/src/logger.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { logs } from '@opentelemetry/api-logs';
|
|
|
7
7
|
import { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';
|
|
8
8
|
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
|
|
9
9
|
import { resourceFromAttributes, defaultResource } from '@opentelemetry/resources';
|
|
10
|
+
import { discoverOrStartCollector } from './collector-manager.js';
|
|
11
|
+
import type { Logger as PinoLogger } from 'pino';
|
|
10
12
|
|
|
11
13
|
// Helper to get require function that works in both CJS and ESM
|
|
12
14
|
declare const require: any;
|
|
@@ -73,11 +75,12 @@ function mapPinoLevelToBeamableLevel(level: number): string {
|
|
|
73
75
|
* @param env - Environment configuration
|
|
74
76
|
* @returns OTLP logger provider if configured, null otherwise
|
|
75
77
|
*/
|
|
76
|
-
function initializeOtlpLogging(
|
|
78
|
+
async function initializeOtlpLogging(
|
|
77
79
|
serviceName?: string,
|
|
78
80
|
qualifiedServiceName?: string,
|
|
79
|
-
env?: EnvironmentConfig
|
|
80
|
-
|
|
81
|
+
env?: EnvironmentConfig,
|
|
82
|
+
logger?: PinoLogger
|
|
83
|
+
): Promise<LoggerProvider | null> {
|
|
81
84
|
// Check for explicit OTLP endpoint (same as C#: BEAM_OTEL_EXPORTER_OTLP_ENDPOINT)
|
|
82
85
|
// Also check standard OTEL environment variables
|
|
83
86
|
const otlpEndpoint = process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
@@ -92,7 +95,7 @@ function initializeOtlpLogging(
|
|
|
92
95
|
|
|
93
96
|
// If no explicit endpoint and standard OTLP not enabled, skip OTLP
|
|
94
97
|
if (!otlpEndpoint && !standardOtelEnabled) {
|
|
95
|
-
return null;
|
|
98
|
+
return Promise.resolve(null);
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
try {
|
|
@@ -117,19 +120,31 @@ function initializeOtlpLogging(
|
|
|
117
120
|
|
|
118
121
|
// Determine endpoint URL
|
|
119
122
|
// If explicit endpoint provided, use it (append /v1/logs if needed for HTTP)
|
|
120
|
-
//
|
|
121
|
-
// For Node.js, we require an explicit endpoint to be configured - we don't assume localhost
|
|
122
|
-
// The OTLP endpoint should be provided via BEAM_OTEL_EXPORTER_OTLP_ENDPOINT environment variable
|
|
123
|
+
// If standard OTLP enabled but no explicit endpoint, try to discover or start collector
|
|
123
124
|
let endpointUrl = otlpEndpoint;
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
if (!endpointUrl && standardOtelEnabled && logger) {
|
|
127
|
+
// Try to discover existing collector or start a new one (like C# does)
|
|
128
|
+
try {
|
|
129
|
+
const discoveredEndpoint = await discoverOrStartCollector(logger, standardOtelEnabled);
|
|
130
|
+
if (discoveredEndpoint) {
|
|
131
|
+
endpointUrl = discoveredEndpoint;
|
|
132
|
+
} else {
|
|
133
|
+
console.error('[OTLP] Standard OTLP is enabled but could not discover or start collector.');
|
|
134
|
+
return Promise.resolve(null);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('[OTLP] Failed to discover/start collector:', error instanceof Error ? error.message : String(error));
|
|
138
|
+
return Promise.resolve(null);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// If still no endpoint, skip OTLP
|
|
127
143
|
if (!endpointUrl) {
|
|
128
144
|
if (standardOtelEnabled) {
|
|
129
|
-
|
|
130
|
-
console.error('[OTLP] Standard OTLP is enabled but no endpoint configured. Set BEAM_OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP logging.');
|
|
145
|
+
console.error('[OTLP] Standard OTLP is enabled but no endpoint available. Set BEAM_OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP logging.');
|
|
131
146
|
}
|
|
132
|
-
return null;
|
|
147
|
+
return Promise.resolve(null);
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
// Ensure endpoint has /v1/logs suffix for HTTP exporter (like C# does)
|
|
@@ -167,7 +182,7 @@ function initializeOtlpLogging(
|
|
|
167
182
|
// This helps diagnose if OTLP is working in deployed environments
|
|
168
183
|
console.error(`[OTLP] OpenTelemetry logging initialized. Endpoint: ${finalEndpointUrl}, Service: ${serviceName || 'unknown'}`);
|
|
169
184
|
|
|
170
|
-
return loggerProvider;
|
|
185
|
+
return Promise.resolve(loggerProvider);
|
|
171
186
|
} catch (error) {
|
|
172
187
|
// If OTLP initialization fails, log error but continue without OTLP
|
|
173
188
|
// Don't throw - we still want stdout logging to work
|
|
@@ -176,7 +191,7 @@ function initializeOtlpLogging(
|
|
|
176
191
|
if (error instanceof Error && error.stack) {
|
|
177
192
|
console.error('[OTLP] Stack trace:', error.stack);
|
|
178
193
|
}
|
|
179
|
-
return null;
|
|
194
|
+
return Promise.resolve(null);
|
|
180
195
|
}
|
|
181
196
|
}
|
|
182
197
|
|
|
@@ -191,7 +206,7 @@ function initializeOtlpLogging(
|
|
|
191
206
|
function createBeamableLogFormatter(
|
|
192
207
|
serviceName?: string,
|
|
193
208
|
qualifiedServiceName?: string,
|
|
194
|
-
|
|
209
|
+
otlpProviderRef?: { provider: LoggerProvider | null }
|
|
195
210
|
): Transform {
|
|
196
211
|
return new Transform({
|
|
197
212
|
objectMode: false, // Pino writes strings/Buffers, not objects
|
|
@@ -362,9 +377,10 @@ function createBeamableLogFormatter(
|
|
|
362
377
|
Object.assign(beamableLog, otelFields);
|
|
363
378
|
|
|
364
379
|
// Send log via OTLP if configured (similar to C# MicroserviceOtelLogRecordExporter)
|
|
365
|
-
if (
|
|
380
|
+
// Check if provider is available (may be null if still initializing)
|
|
381
|
+
if (otlpProviderRef?.provider) {
|
|
366
382
|
try {
|
|
367
|
-
const otlpLogger =
|
|
383
|
+
const otlpLogger = otlpProviderRef.provider.getLogger(
|
|
368
384
|
serviceName || 'beamable-node-runtime',
|
|
369
385
|
undefined, // version
|
|
370
386
|
{
|
|
@@ -422,13 +438,38 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
422
438
|
const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;
|
|
423
439
|
const usePrettyLogs = shouldUsePrettyLogs();
|
|
424
440
|
|
|
425
|
-
//
|
|
441
|
+
// Shared reference for OTLP logger provider (updated asynchronously)
|
|
442
|
+
const otlpProviderRef: { provider: LoggerProvider | null } = { provider: null };
|
|
443
|
+
|
|
444
|
+
// Initialize OTLP logging in the background (similar to C# microservices)
|
|
426
445
|
// This sends logs via OpenTelemetry Protocol in addition to stdout JSON
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
446
|
+
// We start this async so it doesn't block logger creation
|
|
447
|
+
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
448
|
+
const standardOtelEnabled = isInDocker && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
449
|
+
|
|
450
|
+
// Start collector discovery/start in background
|
|
451
|
+
if (standardOtelEnabled || process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
452
|
+
// Create a temporary logger for OTLP initialization
|
|
453
|
+
const tempLogger = pino({
|
|
454
|
+
name: options.name ?? 'beamable-node-runtime',
|
|
455
|
+
level: env.logLevel,
|
|
456
|
+
}, process.stdout);
|
|
457
|
+
|
|
458
|
+
// Initialize OTLP asynchronously (fire-and-forget)
|
|
459
|
+
initializeOtlpLogging(
|
|
460
|
+
options.serviceName,
|
|
461
|
+
options.qualifiedServiceName,
|
|
462
|
+
env,
|
|
463
|
+
tempLogger
|
|
464
|
+
).then((provider) => {
|
|
465
|
+
otlpProviderRef.provider = provider;
|
|
466
|
+
if (provider) {
|
|
467
|
+
tempLogger.info('[OTLP] OpenTelemetry logging initialized successfully');
|
|
468
|
+
}
|
|
469
|
+
}).catch((error) => {
|
|
470
|
+
tempLogger.error(`[OTLP] Failed to initialize: ${error instanceof Error ? error.message : String(error)}`);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
432
473
|
|
|
433
474
|
const pinoOptions: LoggerOptions = {
|
|
434
475
|
name: options.name ?? 'beamable-node-runtime',
|
|
@@ -460,7 +501,7 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
460
501
|
const beamableFormatter = createBeamableLogFormatter(
|
|
461
502
|
options.serviceName,
|
|
462
503
|
options.qualifiedServiceName,
|
|
463
|
-
|
|
504
|
+
otlpProviderRef
|
|
464
505
|
);
|
|
465
506
|
beamableFormatter.pipe(process.stdout);
|
|
466
507
|
return pino(pinoOptions, beamableFormatter);
|
|
@@ -496,7 +537,7 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
496
537
|
const beamableFormatter = createBeamableLogFormatter(
|
|
497
538
|
options.serviceName,
|
|
498
539
|
options.qualifiedServiceName,
|
|
499
|
-
|
|
540
|
+
otlpProviderRef
|
|
500
541
|
);
|
|
501
542
|
const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
502
543
|
beamableFormatter.pipe(fileStream as unknown as NodeJS.WritableStream);
|