@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.
@@ -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
- ): LoggerProvider | null {
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
- const otlpEndpoint = process.env.BEAM_OTEL_EXPORTER_OTLP_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
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
- // Otherwise, for standard OTLP, we'd need to discover it (similar to C# collector discovery)
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
- // Standard OTLP enabled but no endpoint - would need collector discovery here
121
- // For now, skip until we implement collector discovery
122
- return null;
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
- if (endpointUrl && !endpointUrl.includes('/v1/logs')) {
127
- endpointUrl = `${endpointUrl.replace(/\/$/, '')}/v1/logs`;
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: endpointUrl,
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
- return loggerProvider;
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
- console.error('Failed to initialize OTLP logging:', error);
161
- return null;
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
- otlpLoggerProvider?: LoggerProvider | null
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 (otlpLoggerProvider) {
380
+ // Check if provider is available (may be null if still initializing)
381
+ if (otlpProviderRef?.provider) {
348
382
  try {
349
- const otlpLogger = otlpLoggerProvider.getLogger(
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
- // Initialize OTLP logging if configured (similar to C# microservices)
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
- const otlpLoggerProvider = initializeOtlpLogging(
410
- options.serviceName,
411
- options.qualifiedServiceName,
412
- env
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
- otlpLoggerProvider
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
- otlpLoggerProvider
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);