@omen.foundation/node-microservice-runtime 0.1.17 → 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 +62 -20
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +75 -23
- package/dist/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/collector-manager.ts +299 -0
- package/src/logger.ts +88 -29
|
@@ -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,22 +75,27 @@ 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
|
|
86
|
+
const otlpEndpoint = process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT
|
|
87
|
+
|| process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
88
|
+
|| (process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS ? process.env.OTEL_EXPORTER_OTLP_ENDPOINT_LOGS : null);
|
|
83
89
|
|
|
84
90
|
// Check if standard OTLP is enabled (similar to C# OtelExporterStandardEnabled)
|
|
85
|
-
// In Docker, OTLP should be enabled unless explicitly disabled
|
|
91
|
+
// In Docker containers, OTLP should be enabled unless explicitly disabled
|
|
92
|
+
// Simple check: if IS_LOCAL is not set, we're likely in a container
|
|
86
93
|
const isInDocker = process.env.IS_LOCAL !== '1' && process.env.IS_LOCAL !== 'true';
|
|
87
94
|
const standardOtelEnabled = isInDocker && !process.env.BEAM_DISABLE_STANDARD_OTEL;
|
|
88
95
|
|
|
89
96
|
// If no explicit endpoint and standard OTLP not enabled, skip OTLP
|
|
90
97
|
if (!otlpEndpoint && !standardOtelEnabled) {
|
|
91
|
-
return null;
|
|
98
|
+
return Promise.resolve(null);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
try {
|
|
@@ -113,23 +120,41 @@ function initializeOtlpLogging(
|
|
|
113
120
|
|
|
114
121
|
// Determine endpoint URL
|
|
115
122
|
// If explicit endpoint provided, use it (append /v1/logs if needed for HTTP)
|
|
116
|
-
//
|
|
117
|
-
// For now, if no explicit endpoint, we skip (collector discovery would go here)
|
|
123
|
+
// If standard OTLP enabled but no explicit endpoint, try to discover or start collector
|
|
118
124
|
let endpointUrl = otlpEndpoint;
|
|
125
|
+
|
|
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
|
|
119
143
|
if (!endpointUrl) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
if (standardOtelEnabled) {
|
|
145
|
+
console.error('[OTLP] Standard OTLP is enabled but no endpoint available. Set BEAM_OTEL_EXPORTER_OTLP_ENDPOINT to enable OTLP logging.');
|
|
146
|
+
}
|
|
147
|
+
return Promise.resolve(null);
|
|
123
148
|
}
|
|
124
149
|
|
|
125
150
|
// Ensure endpoint has /v1/logs suffix for HTTP exporter (like C# does)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
151
|
+
const finalEndpointUrl = endpointUrl.includes('/v1/logs')
|
|
152
|
+
? endpointUrl
|
|
153
|
+
: `${endpointUrl.replace(/\/$/, '')}/v1/logs`;
|
|
129
154
|
|
|
130
155
|
// Create OTLP HTTP exporter (C# uses HttpProtobuf by default)
|
|
131
156
|
const exporter = new OTLPLogExporter({
|
|
132
|
-
url:
|
|
157
|
+
url: finalEndpointUrl,
|
|
133
158
|
// Headers if provided (similar to C# OtelExporterOtlpHeaders)
|
|
134
159
|
headers: process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS
|
|
135
160
|
? JSON.parse(process.env.BEAM_OTEL_EXPORTER_OTLP_HEADERS)
|
|
@@ -153,12 +178,20 @@ function initializeOtlpLogging(
|
|
|
153
178
|
// Set as global logger provider
|
|
154
179
|
logs.setGlobalLoggerProvider(loggerProvider);
|
|
155
180
|
|
|
156
|
-
|
|
181
|
+
// Log successful initialization (to stdout for debugging)
|
|
182
|
+
// This helps diagnose if OTLP is working in deployed environments
|
|
183
|
+
console.error(`[OTLP] OpenTelemetry logging initialized. Endpoint: ${finalEndpointUrl}, Service: ${serviceName || 'unknown'}`);
|
|
184
|
+
|
|
185
|
+
return Promise.resolve(loggerProvider);
|
|
157
186
|
} catch (error) {
|
|
158
187
|
// If OTLP initialization fails, log error but continue without OTLP
|
|
159
188
|
// Don't throw - we still want stdout logging to work
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
// Log to stderr so it's visible in container logs
|
|
190
|
+
console.error('[OTLP] Failed to initialize OTLP logging:', error instanceof Error ? error.message : String(error));
|
|
191
|
+
if (error instanceof Error && error.stack) {
|
|
192
|
+
console.error('[OTLP] Stack trace:', error.stack);
|
|
193
|
+
}
|
|
194
|
+
return Promise.resolve(null);
|
|
162
195
|
}
|
|
163
196
|
}
|
|
164
197
|
|
|
@@ -173,7 +206,7 @@ function initializeOtlpLogging(
|
|
|
173
206
|
function createBeamableLogFormatter(
|
|
174
207
|
serviceName?: string,
|
|
175
208
|
qualifiedServiceName?: string,
|
|
176
|
-
|
|
209
|
+
otlpProviderRef?: { provider: LoggerProvider | null }
|
|
177
210
|
): Transform {
|
|
178
211
|
return new Transform({
|
|
179
212
|
objectMode: false, // Pino writes strings/Buffers, not objects
|
|
@@ -344,9 +377,10 @@ function createBeamableLogFormatter(
|
|
|
344
377
|
Object.assign(beamableLog, otelFields);
|
|
345
378
|
|
|
346
379
|
// Send log via OTLP if configured (similar to C# MicroserviceOtelLogRecordExporter)
|
|
347
|
-
if (
|
|
380
|
+
// Check if provider is available (may be null if still initializing)
|
|
381
|
+
if (otlpProviderRef?.provider) {
|
|
348
382
|
try {
|
|
349
|
-
const otlpLogger =
|
|
383
|
+
const otlpLogger = otlpProviderRef.provider.getLogger(
|
|
350
384
|
serviceName || 'beamable-node-runtime',
|
|
351
385
|
undefined, // version
|
|
352
386
|
{
|
|
@@ -404,13 +438,38 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
404
438
|
const configuredDestination = options.destinationPath ?? process.env.LOG_PATH;
|
|
405
439
|
const usePrettyLogs = shouldUsePrettyLogs();
|
|
406
440
|
|
|
407
|
-
//
|
|
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)
|
|
408
445
|
// This sends logs via OpenTelemetry Protocol in addition to stdout JSON
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
}
|
|
414
473
|
|
|
415
474
|
const pinoOptions: LoggerOptions = {
|
|
416
475
|
name: options.name ?? 'beamable-node-runtime',
|
|
@@ -442,7 +501,7 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
442
501
|
const beamableFormatter = createBeamableLogFormatter(
|
|
443
502
|
options.serviceName,
|
|
444
503
|
options.qualifiedServiceName,
|
|
445
|
-
|
|
504
|
+
otlpProviderRef
|
|
446
505
|
);
|
|
447
506
|
beamableFormatter.pipe(process.stdout);
|
|
448
507
|
return pino(pinoOptions, beamableFormatter);
|
|
@@ -478,7 +537,7 @@ export function createLogger(env: EnvironmentConfig, options: LoggerFactoryOptio
|
|
|
478
537
|
const beamableFormatter = createBeamableLogFormatter(
|
|
479
538
|
options.serviceName,
|
|
480
539
|
options.qualifiedServiceName,
|
|
481
|
-
|
|
540
|
+
otlpProviderRef
|
|
482
541
|
);
|
|
483
542
|
const fileStream = destination({ dest: resolvedDestination, mkdir: true, append: true, sync: false });
|
|
484
543
|
beamableFormatter.pipe(fileStream as unknown as NodeJS.WritableStream);
|