@swirepay-developer/common-logging-nodejs 1.0.0

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/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Swirepay Common Logging - Node.js SDK
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ npm install @swirepay/common-logging-nodejs
7
+ ```
8
+
9
+ ## Quick Start
10
+
11
+ ```javascript
12
+ const { initializeLogger, getLogger } = require('@swirepay/common-logging-nodejs');
13
+
14
+ // OpenTelemetry is automatically initialized
15
+ initializeLogger({
16
+ environment: 'staging',
17
+ serviceName: 'my-service',
18
+ serviceVersion: '1.0.0',
19
+ });
20
+
21
+ const logger = getLogger();
22
+ logger.info('Service started');
23
+ ```
24
+
25
+ ## Complete Documentation
26
+
27
+ See [../USAGE_GUIDE.md](../USAGE_GUIDE.md) for complete examples including:
28
+ - Basic logging
29
+ - Express.js integration
30
+ - API call logging
31
+ - Request logging
32
+ - Webhook logging
33
+ - TypeScript examples
34
+
35
+ ## License
36
+
37
+ Proprietary - Swirepay
@@ -0,0 +1,10 @@
1
+ export interface LoggingConfig {
2
+ environment: 'staging' | 'production';
3
+ serviceName: string;
4
+ serviceVersion: string;
5
+ lokiUrl?: string;
6
+ enableConsole?: boolean;
7
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
8
+ }
9
+ export declare function getLokiUrl(environment: 'staging' | 'production'): string;
10
+ export declare function getDefaultConfig(): Partial<LoggingConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getLokiUrl = getLokiUrl;
4
+ exports.getDefaultConfig = getDefaultConfig;
5
+ const STAG_LOKI_URL = 'https://stag-loki.swirepay.com:443/loki/api/v1/push';
6
+ const PROD_LOKI_URL = 'https://loki.swirepay.com:443/loki/api/v1/push';
7
+ function getLokiUrl(environment) {
8
+ return environment === 'production' ? PROD_LOKI_URL : STAG_LOKI_URL;
9
+ }
10
+ function getDefaultConfig() {
11
+ return {
12
+ environment: process.env.ENVIRONMENT || 'staging',
13
+ serviceName: process.env.SERVICE_NAME || 'swirepay-service',
14
+ serviceVersion: process.env.SERVICE_VERSION || '1.0.0',
15
+ enableConsole: process.env.ENABLE_CONSOLE_LOG === 'true',
16
+ logLevel: process.env.LOG_LEVEL || 'info',
17
+ };
18
+ }
@@ -0,0 +1,5 @@
1
+ export { SwirepayLogger, initializeLogger, getLogger, TraceContext } from './logger';
2
+ export { LoggingConfig, getLokiUrl, getDefaultConfig } from './config';
3
+ export { LokiTransport } from './loki-transport';
4
+ export { OTEL_ENDPOINTS, getOtelEndpoint } from './otel-config';
5
+ export { initOpenTelemetry, OpenTelemetryConfig } from './otel-init';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initOpenTelemetry = exports.getOtelEndpoint = exports.OTEL_ENDPOINTS = exports.LokiTransport = exports.getDefaultConfig = exports.getLokiUrl = exports.getLogger = exports.initializeLogger = exports.SwirepayLogger = void 0;
4
+ var logger_1 = require("./logger");
5
+ Object.defineProperty(exports, "SwirepayLogger", { enumerable: true, get: function () { return logger_1.SwirepayLogger; } });
6
+ Object.defineProperty(exports, "initializeLogger", { enumerable: true, get: function () { return logger_1.initializeLogger; } });
7
+ Object.defineProperty(exports, "getLogger", { enumerable: true, get: function () { return logger_1.getLogger; } });
8
+ var config_1 = require("./config");
9
+ Object.defineProperty(exports, "getLokiUrl", { enumerable: true, get: function () { return config_1.getLokiUrl; } });
10
+ Object.defineProperty(exports, "getDefaultConfig", { enumerable: true, get: function () { return config_1.getDefaultConfig; } });
11
+ var loki_transport_1 = require("./loki-transport");
12
+ Object.defineProperty(exports, "LokiTransport", { enumerable: true, get: function () { return loki_transport_1.LokiTransport; } });
13
+ var otel_config_1 = require("./otel-config");
14
+ Object.defineProperty(exports, "OTEL_ENDPOINTS", { enumerable: true, get: function () { return otel_config_1.OTEL_ENDPOINTS; } });
15
+ Object.defineProperty(exports, "getOtelEndpoint", { enumerable: true, get: function () { return otel_config_1.getOtelEndpoint; } });
16
+ var otel_init_1 = require("./otel-init");
17
+ Object.defineProperty(exports, "initOpenTelemetry", { enumerable: true, get: function () { return otel_init_1.initOpenTelemetry; } });
@@ -0,0 +1,64 @@
1
+ import { LoggingConfig } from './config';
2
+ export interface TraceContext {
3
+ traceId: string;
4
+ spanId: string;
5
+ }
6
+ export declare class SwirepayLogger {
7
+ private logger;
8
+ private config;
9
+ constructor(config?: Partial<LoggingConfig>);
10
+ /**
11
+ * Generate a new trace context with unique trace-id and span-id
12
+ * @returns New trace context with 32-char hex trace-id and 16-char hex span-id
13
+ */
14
+ generateTraceContext(): TraceContext;
15
+ /**
16
+ * Get trace context from OpenTelemetry active span or create a new span
17
+ * @param customTraceContext Optional custom trace context to use instead of OpenTelemetry
18
+ * @returns Trace context object
19
+ */
20
+ private getTraceContext;
21
+ private log;
22
+ debug(message: string, meta?: any, traceContext?: TraceContext): void;
23
+ info(message: string, meta?: any, traceContext?: TraceContext): void;
24
+ warn(message: string, meta?: any, traceContext?: TraceContext): void;
25
+ error(message: string, error?: Error | any, meta?: any, traceContext?: TraceContext): void;
26
+ logApiCall(options: {
27
+ method: string;
28
+ url: string;
29
+ statusCode?: number;
30
+ duration?: number;
31
+ requestBody?: any;
32
+ responseBody?: any;
33
+ headers?: Record<string, string>;
34
+ error?: Error;
35
+ traceContext?: TraceContext;
36
+ }): void;
37
+ logRequest(options: {
38
+ method: string;
39
+ path: string;
40
+ statusCode?: number;
41
+ duration?: number;
42
+ userId?: string;
43
+ ip?: string;
44
+ userAgent?: string;
45
+ requestBody?: any;
46
+ responseBody?: any;
47
+ headers?: Record<string, string>;
48
+ error?: Error;
49
+ traceContext?: TraceContext;
50
+ }): void;
51
+ logWebhook(options: {
52
+ source: string;
53
+ event: string;
54
+ webhookId?: string;
55
+ status: 'received' | 'processed' | 'failed';
56
+ payload?: any;
57
+ error?: Error;
58
+ processingTime?: number;
59
+ traceContext?: TraceContext;
60
+ }): void;
61
+ private sanitizeBody;
62
+ }
63
+ export declare function initializeLogger(config?: Partial<LoggingConfig>): SwirepayLogger;
64
+ export declare function getLogger(): SwirepayLogger;
package/dist/logger.js ADDED
@@ -0,0 +1,305 @@
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.SwirepayLogger = void 0;
7
+ exports.initializeLogger = initializeLogger;
8
+ exports.getLogger = getLogger;
9
+ const winston_1 = __importDefault(require("winston"));
10
+ const config_1 = require("./config");
11
+ const loki_transport_1 = require("./loki-transport");
12
+ const otel_transport_1 = require("./otel-transport");
13
+ const api_1 = require("@opentelemetry/api");
14
+ const otel_init_1 = require("./otel-init");
15
+ const crypto_1 = require("crypto");
16
+ class SwirepayLogger {
17
+ constructor(config = {}) {
18
+ const defaultConfig = (0, config_1.getDefaultConfig)();
19
+ this.config = { ...defaultConfig, ...config };
20
+ const lokiUrl = this.config.lokiUrl || (0, config_1.getLokiUrl)(this.config.environment);
21
+ const transports = [
22
+ // Always send to Loki
23
+ new loki_transport_1.LokiTransport({
24
+ lokiUrl,
25
+ environment: this.config.environment,
26
+ serviceName: this.config.serviceName,
27
+ serviceVersion: this.config.serviceVersion,
28
+ }),
29
+ // Automatically send to OpenTelemetry if initialized
30
+ new otel_transport_1.OtelTransport({
31
+ environment: this.config.environment,
32
+ serviceName: this.config.serviceName,
33
+ serviceVersion: this.config.serviceVersion,
34
+ }),
35
+ ];
36
+ if (this.config.enableConsole) {
37
+ transports.push(new winston_1.default.transports.Console({
38
+ format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json()),
39
+ }));
40
+ }
41
+ this.logger = winston_1.default.createLogger({
42
+ level: this.config.logLevel || 'info',
43
+ format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.json()),
44
+ transports,
45
+ defaultMeta: {
46
+ service: this.config.serviceName,
47
+ version: this.config.serviceVersion,
48
+ environment: this.config.environment,
49
+ },
50
+ });
51
+ }
52
+ /**
53
+ * Generate a new trace context with unique trace-id and span-id
54
+ * @returns New trace context with 32-char hex trace-id and 16-char hex span-id
55
+ */
56
+ generateTraceContext() {
57
+ return {
58
+ traceId: (0, crypto_1.randomBytes)(16).toString('hex'), // 32 character hex string
59
+ spanId: (0, crypto_1.randomBytes)(8).toString('hex'), // 16 character hex string
60
+ };
61
+ }
62
+ /**
63
+ * Get trace context from OpenTelemetry active span or create a new span
64
+ * @param customTraceContext Optional custom trace context to use instead of OpenTelemetry
65
+ * @returns Trace context object
66
+ */
67
+ getTraceContext(customTraceContext) {
68
+ // If custom trace context is provided, use it
69
+ if (customTraceContext) {
70
+ return customTraceContext;
71
+ }
72
+ // Try to get from OpenTelemetry active span
73
+ const activeSpan = api_1.trace.getActiveSpan();
74
+ if (activeSpan) {
75
+ const spanContext = activeSpan.spanContext();
76
+ // Check if trace ID is valid (not all zeros)
77
+ if (spanContext.traceId && spanContext.traceId !== '00000000000000000000000000000000') {
78
+ return {
79
+ traceId: spanContext.traceId,
80
+ spanId: spanContext.spanId,
81
+ };
82
+ }
83
+ }
84
+ // If no active span, create a new one for this log
85
+ // This ensures we always have trace context even if called outside a request context
86
+ try {
87
+ const tracer = api_1.trace.getTracer(this.config.serviceName, this.config.serviceVersion);
88
+ const span = tracer.startSpan('swirepay-log');
89
+ const spanContext = span.spanContext();
90
+ span.end();
91
+ if (spanContext.traceId && spanContext.traceId !== '00000000000000000000000000000000') {
92
+ return {
93
+ traceId: spanContext.traceId,
94
+ spanId: spanContext.spanId,
95
+ };
96
+ }
97
+ }
98
+ catch (error) {
99
+ // If OpenTelemetry isn't ready yet, fall back to generated trace context
100
+ }
101
+ // Fallback: generate new trace context
102
+ return this.generateTraceContext();
103
+ }
104
+ log(level, message, meta = {}, traceContext) {
105
+ const trace = this.getTraceContext(traceContext);
106
+ this.logger.log(level, message, {
107
+ ...meta,
108
+ ...trace,
109
+ });
110
+ }
111
+ debug(message, meta, traceContext) {
112
+ this.log('debug', message, meta, traceContext);
113
+ }
114
+ info(message, meta, traceContext) {
115
+ this.log('info', message, meta, traceContext);
116
+ }
117
+ warn(message, meta, traceContext) {
118
+ this.log('warn', message, meta, traceContext);
119
+ }
120
+ error(message, error, meta, traceContext) {
121
+ const errorMeta = {
122
+ ...meta,
123
+ error: {
124
+ message: error?.message || String(error),
125
+ stack: error?.stack,
126
+ name: error?.name,
127
+ },
128
+ };
129
+ this.log('error', message || error?.message || 'Error occurred', errorMeta, traceContext);
130
+ }
131
+ logApiCall(options) {
132
+ const { method, url, statusCode, duration, requestBody, responseBody, headers, error, traceContext } = options;
133
+ const trace = this.getTraceContext(traceContext);
134
+ const logData = {
135
+ method: method.toUpperCase(),
136
+ url,
137
+ eventType: 'api_call',
138
+ apiCall: {
139
+ method: method.toUpperCase(),
140
+ url,
141
+ statusCode,
142
+ duration,
143
+ timestamp: new Date().toISOString(),
144
+ },
145
+ };
146
+ if (statusCode)
147
+ logData.statusCode = statusCode;
148
+ if (duration !== undefined)
149
+ logData.apiCall.duration_ms = duration;
150
+ if (headers)
151
+ logData.apiCall.headers = headers;
152
+ if (requestBody)
153
+ logData.apiCall.request_body = this.sanitizeBody(requestBody);
154
+ if (responseBody)
155
+ logData.apiCall.response_body = this.sanitizeBody(responseBody);
156
+ if (error) {
157
+ logData.error = {
158
+ message: error.message,
159
+ stack: error.stack,
160
+ name: error.name,
161
+ };
162
+ }
163
+ const level = error || (statusCode && statusCode >= 400) ? 'error' : 'info';
164
+ const message = `API Call: ${method.toUpperCase()} ${url}${statusCode ? ` - ${statusCode}` : ''}${duration ? ` (${duration}ms)` : ''}`;
165
+ this.logger.log(level, message, {
166
+ ...logData,
167
+ ...trace,
168
+ });
169
+ }
170
+ logRequest(options) {
171
+ const { method, path, statusCode, duration, userId, ip, userAgent, requestBody, responseBody, headers, error, traceContext } = options;
172
+ const trace = this.getTraceContext(traceContext);
173
+ const logData = {
174
+ method: method.toUpperCase(),
175
+ url: path,
176
+ eventType: 'request',
177
+ request: {
178
+ method: method.toUpperCase(),
179
+ path,
180
+ statusCode,
181
+ duration,
182
+ timestamp: new Date().toISOString(),
183
+ },
184
+ };
185
+ if (statusCode)
186
+ logData.statusCode = statusCode;
187
+ if (duration !== undefined)
188
+ logData.request.duration_ms = duration;
189
+ if (userId)
190
+ logData.request.user_id = userId;
191
+ if (ip)
192
+ logData.request.ip = ip;
193
+ if (userAgent)
194
+ logData.request.user_agent = userAgent;
195
+ if (headers)
196
+ logData.request.headers = headers;
197
+ if (requestBody)
198
+ logData.request.request_body = this.sanitizeBody(requestBody);
199
+ if (responseBody)
200
+ logData.request.response_body = this.sanitizeBody(responseBody);
201
+ if (error) {
202
+ logData.error = {
203
+ message: error.message,
204
+ stack: error.stack,
205
+ name: error.name,
206
+ };
207
+ }
208
+ const level = error || (statusCode && statusCode >= 400) ? 'error' : 'info';
209
+ const message = `Request: ${method.toUpperCase()} ${path}${statusCode ? ` - ${statusCode}` : ''}${duration ? ` (${duration}ms)` : ''}`;
210
+ this.logger.log(level, message, {
211
+ ...logData,
212
+ ...trace,
213
+ });
214
+ }
215
+ logWebhook(options) {
216
+ const { source, event, webhookId, status, payload, error, processingTime, traceContext } = options;
217
+ const trace = this.getTraceContext(traceContext);
218
+ const logData = {
219
+ eventType: 'webhook',
220
+ webhook: {
221
+ source,
222
+ event,
223
+ status,
224
+ timestamp: new Date().toISOString(),
225
+ },
226
+ };
227
+ if (webhookId)
228
+ logData.webhook.webhook_id = webhookId;
229
+ if (processingTime !== undefined)
230
+ logData.webhook.processing_time_ms = processingTime;
231
+ if (payload)
232
+ logData.webhook.payload = this.sanitizeBody(payload);
233
+ if (error) {
234
+ logData.error = {
235
+ message: error.message,
236
+ stack: error.stack,
237
+ name: error.name,
238
+ };
239
+ }
240
+ const level = error || status === 'failed' ? 'error' : 'info';
241
+ const message = `Webhook: ${source} - ${event} (${status})`;
242
+ this.logger.log(level, message, {
243
+ ...logData,
244
+ ...trace,
245
+ });
246
+ }
247
+ sanitizeBody(body) {
248
+ if (!body)
249
+ return body;
250
+ if (typeof body !== 'object')
251
+ return body;
252
+ const sensitiveFields = ['password', 'token', 'secret', 'apiKey', 'api_key', 'authorization', 'creditCard', 'credit_card', 'cvv', 'ssn'];
253
+ const sanitized = { ...body };
254
+ for (const key in sanitized) {
255
+ if (sensitiveFields.some(field => key.toLowerCase().includes(field.toLowerCase()))) {
256
+ sanitized[key] = '***REDACTED***';
257
+ }
258
+ else if (typeof sanitized[key] === 'object' && sanitized[key] !== null) {
259
+ sanitized[key] = this.sanitizeBody(sanitized[key]);
260
+ }
261
+ }
262
+ return sanitized;
263
+ }
264
+ }
265
+ exports.SwirepayLogger = SwirepayLogger;
266
+ let defaultLogger = null;
267
+ function initializeLogger(config) {
268
+ // Get config with defaults
269
+ const defaultConfig = (0, config_1.getDefaultConfig)();
270
+ const finalConfig = { ...defaultConfig, ...config };
271
+ // Automatically initialize OpenTelemetry (SDK handles this internally)
272
+ try {
273
+ (0, otel_init_1.initOpenTelemetry)({
274
+ serviceName: finalConfig.serviceName,
275
+ serviceVersion: finalConfig.serviceVersion,
276
+ environment: finalConfig.environment,
277
+ });
278
+ }
279
+ catch (error) {
280
+ throw new Error(`Failed to initialize OpenTelemetry: ${error instanceof Error ? error.message : String(error)}. ` +
281
+ 'OpenTelemetry is required for SwirepayLogger.');
282
+ }
283
+ defaultLogger = new SwirepayLogger(config);
284
+ return defaultLogger;
285
+ }
286
+ function getLogger() {
287
+ if (!defaultLogger) {
288
+ // Automatically initialize with defaults if not already initialized
289
+ // This ensures OpenTelemetry is initialized automatically
290
+ const defaultConfig = (0, config_1.getDefaultConfig)();
291
+ try {
292
+ (0, otel_init_1.initOpenTelemetry)({
293
+ serviceName: defaultConfig.serviceName || 'swirepay-service',
294
+ serviceVersion: defaultConfig.serviceVersion || '1.0.0',
295
+ environment: defaultConfig.environment || 'staging',
296
+ });
297
+ }
298
+ catch (error) {
299
+ throw new Error(`Failed to initialize OpenTelemetry: ${error instanceof Error ? error.message : String(error)}. ` +
300
+ 'OpenTelemetry is required for SwirepayLogger.');
301
+ }
302
+ defaultLogger = new SwirepayLogger();
303
+ }
304
+ return defaultLogger;
305
+ }
@@ -0,0 +1,18 @@
1
+ import Transport from 'winston-transport';
2
+ import { LogEntry } from 'winston';
3
+ export declare class LokiTransport extends Transport {
4
+ private lokiUrl;
5
+ private batch;
6
+ private batchTimeout;
7
+ private readonly batchSize;
8
+ private readonly batchInterval;
9
+ constructor(opts: {
10
+ lokiUrl: string;
11
+ environment: string;
12
+ serviceName: string;
13
+ serviceVersion: string;
14
+ });
15
+ log(info: LogEntry, callback: () => void): void;
16
+ private flush;
17
+ close(): Promise<void>;
18
+ }
@@ -0,0 +1,118 @@
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.LokiTransport = void 0;
7
+ const winston_transport_1 = __importDefault(require("winston-transport"));
8
+ class LokiTransport extends winston_transport_1.default {
9
+ constructor(opts) {
10
+ super();
11
+ this.batch = [];
12
+ this.batchTimeout = null;
13
+ this.batchSize = 100;
14
+ this.batchInterval = 5000; // 5 seconds
15
+ this.lokiUrl = opts.lokiUrl;
16
+ }
17
+ log(info, callback) {
18
+ setImmediate(() => {
19
+ this.emit('logged', info);
20
+ });
21
+ const timestamp = new Date().toISOString();
22
+ const nanoTimestamp = `${Date.now()}000000`; // Convert to nanoseconds
23
+ // Extract log level and message
24
+ const level = info.level || 'info';
25
+ const message = info.message || JSON.stringify(info);
26
+ // Build labels for Loki stream
27
+ const stream = {
28
+ level: level.toUpperCase(),
29
+ service_name: info.service || 'unknown',
30
+ environment: info.environment || 'unknown',
31
+ version: info.version || 'unknown',
32
+ };
33
+ // Add custom labels from info object
34
+ if (info.traceId)
35
+ stream.trace_id = info.traceId;
36
+ if (info.spanId)
37
+ stream.span_id = info.spanId;
38
+ if (info.method)
39
+ stream.method = info.method;
40
+ if (info.url)
41
+ stream.url = info.url;
42
+ if (info.statusCode)
43
+ stream.status_code = String(info.statusCode);
44
+ if (info.eventType)
45
+ stream.event_type = info.eventType;
46
+ // Build log line with structured data
47
+ const logLine = {
48
+ timestamp,
49
+ level: level.toUpperCase(),
50
+ message,
51
+ service: {
52
+ name: info.service || 'unknown',
53
+ version: info.version || 'unknown',
54
+ },
55
+ environment: info.environment || 'unknown',
56
+ };
57
+ // Add trace context
58
+ if (info.traceId)
59
+ logLine.trace_id = info.traceId;
60
+ if (info.spanId)
61
+ logLine.span_id = info.spanId;
62
+ // Add attributes
63
+ if (info.attributes) {
64
+ logLine.attributes = info.attributes;
65
+ }
66
+ // Add specific fields based on log type
67
+ if (info.apiCall) {
68
+ logLine.api_call = info.apiCall;
69
+ }
70
+ if (info.request) {
71
+ logLine.request = info.request;
72
+ }
73
+ if (info.webhook) {
74
+ logLine.webhook = info.webhook;
75
+ }
76
+ const logEntry = {
77
+ stream,
78
+ values: [[nanoTimestamp, JSON.stringify(logLine)]],
79
+ };
80
+ this.batch.push(logEntry);
81
+ if (this.batch.length >= this.batchSize) {
82
+ this.flush();
83
+ }
84
+ else if (!this.batchTimeout) {
85
+ this.batchTimeout = setTimeout(() => this.flush(), this.batchInterval);
86
+ }
87
+ callback();
88
+ }
89
+ async flush() {
90
+ if (this.batch.length === 0)
91
+ return;
92
+ const batchToSend = [...this.batch];
93
+ this.batch = [];
94
+ if (this.batchTimeout) {
95
+ clearTimeout(this.batchTimeout);
96
+ this.batchTimeout = null;
97
+ }
98
+ try {
99
+ const response = await fetch(this.lokiUrl, {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Content-Type': 'application/json',
103
+ },
104
+ body: JSON.stringify({ streams: batchToSend }),
105
+ });
106
+ if (!response.ok) {
107
+ console.error(`Failed to send logs to Loki: ${response.status} ${response.statusText}`);
108
+ }
109
+ }
110
+ catch (error) {
111
+ console.error('Error sending logs to Loki:', error);
112
+ }
113
+ }
114
+ async close() {
115
+ await this.flush();
116
+ }
117
+ }
118
+ exports.LokiTransport = LokiTransport;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenTelemetry Configuration
3
+ * Centralized configuration for OpenTelemetry Collector endpoints
4
+ *
5
+ * Endpoints can be overridden via environment variables:
6
+ * - OTEL_EXPORTER_OTLP_ENDPOINT (base URL, e.g., "http://localhost:4318")
7
+ * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (full traces URL)
8
+ * - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT (full logs URL)
9
+ * - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT (full metrics URL)
10
+ */
11
+ export declare const OTEL_ENDPOINTS: {
12
+ readonly STAGING: {
13
+ readonly base: "https://stag-otel.swirepay.com";
14
+ readonly traces: "https://stag-otel.swirepay.com/v1/traces";
15
+ readonly logs: "https://stag-otel.swirepay.com/v1/logs";
16
+ readonly metrics: "https://stag-otel.swirepay.com/v1/metrics";
17
+ };
18
+ readonly PRODUCTION: {
19
+ readonly base: "https://otel.swirepay.com";
20
+ readonly traces: "https://otel.swirepay.com/v1/traces";
21
+ readonly logs: "https://otel.swirepay.com/v1/logs";
22
+ readonly metrics: "https://otel.swirepay.com/v1/metrics";
23
+ };
24
+ };
25
+ export declare function getOtelEndpoint(environment: "staging" | "production", type?: "base" | "traces" | "logs" | "metrics"): string;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * OpenTelemetry Configuration
4
+ * Centralized configuration for OpenTelemetry Collector endpoints
5
+ *
6
+ * Endpoints can be overridden via environment variables:
7
+ * - OTEL_EXPORTER_OTLP_ENDPOINT (base URL, e.g., "http://localhost:4318")
8
+ * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (full traces URL)
9
+ * - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT (full logs URL)
10
+ * - OTEL_EXPORTER_OTLP_METRICS_ENDPOINT (full metrics URL)
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.OTEL_ENDPOINTS = void 0;
14
+ exports.getOtelEndpoint = getOtelEndpoint;
15
+ exports.OTEL_ENDPOINTS = {
16
+ STAGING: {
17
+ base: "https://stag-otel.swirepay.com",
18
+ traces: "https://stag-otel.swirepay.com/v1/traces",
19
+ logs: "https://stag-otel.swirepay.com/v1/logs",
20
+ metrics: "https://stag-otel.swirepay.com/v1/metrics",
21
+ },
22
+ PRODUCTION: {
23
+ base: "https://otel.swirepay.com",
24
+ traces: "https://otel.swirepay.com/v1/traces",
25
+ logs: "https://otel.swirepay.com/v1/logs",
26
+ metrics: "https://otel.swirepay.com/v1/metrics",
27
+ },
28
+ };
29
+ function getOtelEndpoint(environment, type = "base") {
30
+ // Check for environment variable overrides first
31
+ const envVarMap = {
32
+ base: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
33
+ traces: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
34
+ logs: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
35
+ metrics: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
36
+ };
37
+ // If specific endpoint type is set, use it
38
+ if (envVarMap[type]) {
39
+ return envVarMap[type];
40
+ }
41
+ // If base endpoint is set, construct the full URL
42
+ if (type !== "base" && envVarMap.base) {
43
+ const base = envVarMap.base.replace(/\/$/, ""); // Remove trailing slash
44
+ return `${base}/v1/${type}`;
45
+ }
46
+ // Fallback to default endpoints
47
+ const endpoints = environment === "production" ? exports.OTEL_ENDPOINTS.PRODUCTION : exports.OTEL_ENDPOINTS.STAGING;
48
+ return endpoints[type];
49
+ }
@@ -0,0 +1,14 @@
1
+ import { NodeSDK } from "@opentelemetry/sdk-node";
2
+ export interface OpenTelemetryConfig {
3
+ serviceName?: string;
4
+ serviceVersion?: string;
5
+ environment?: string;
6
+ }
7
+ /**
8
+ * Initialize OpenTelemetry with automatic instrumentation and log export
9
+ * This is called automatically by the SDK - users don't need to call this
10
+ * @param config Configuration options for OpenTelemetry
11
+ * @returns NodeSDK instance
12
+ * @throws Error if initialization fails
13
+ */
14
+ export declare function initOpenTelemetry({ serviceName, serviceVersion, environment, }?: OpenTelemetryConfig): NodeSDK;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.initOpenTelemetry = initOpenTelemetry;
4
+ const sdk_node_1 = require("@opentelemetry/sdk-node");
5
+ const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
6
+ const resources_1 = require("@opentelemetry/resources");
7
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
8
+ const exporter_otlp_proto_1 = require("@opentelemetry/exporter-otlp-proto");
9
+ const exporter_logs_otlp_proto_1 = require("@opentelemetry/exporter-logs-otlp-proto");
10
+ const api_logs_1 = require("@opentelemetry/api-logs");
11
+ const sdk_logs_1 = require("@opentelemetry/sdk-logs");
12
+ const otel_config_1 = require("./otel-config");
13
+ let isInitialized = false;
14
+ /**
15
+ * Initialize OpenTelemetry with automatic instrumentation and log export
16
+ * This is called automatically by the SDK - users don't need to call this
17
+ * @param config Configuration options for OpenTelemetry
18
+ * @returns NodeSDK instance
19
+ * @throws Error if initialization fails
20
+ */
21
+ function initOpenTelemetry({ serviceName = "unknown-service", serviceVersion = "0.0.0", environment = process.env.ENVIRONMENT || "staging", } = {}) {
22
+ // Only initialize once
23
+ if (isInitialized) {
24
+ return global.__swirepayOtelSdk;
25
+ }
26
+ try {
27
+ // Ensure environment is valid type
28
+ const env = environment === "production" ? "production" : "staging";
29
+ // Get OpenTelemetry endpoints from centralized config
30
+ const otelTracesEndpoint = (0, otel_config_1.getOtelEndpoint)(env, "traces");
31
+ const otelLogsEndpoint = (0, otel_config_1.getOtelEndpoint)(env, "logs");
32
+ // Configure OpenTelemetry Logger Provider for logs
33
+ const logResource = new resources_1.Resource({
34
+ [semantic_conventions_1.SemanticResourceAttributes.SERVICE_NAME]: serviceName,
35
+ [semantic_conventions_1.SemanticResourceAttributes.SERVICE_VERSION]: serviceVersion,
36
+ [semantic_conventions_1.SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env,
37
+ });
38
+ const loggerProvider = new sdk_logs_1.LoggerProvider({
39
+ resource: logResource,
40
+ });
41
+ const logExporter = new exporter_logs_otlp_proto_1.OTLPLogExporter({
42
+ url: otelLogsEndpoint,
43
+ });
44
+ // Use BatchLogRecordProcessor for better performance
45
+ const { BatchLogRecordProcessor } = require("@opentelemetry/sdk-logs");
46
+ loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter));
47
+ api_logs_1.logs.setGlobalLoggerProvider(loggerProvider);
48
+ // Configure OpenTelemetry SDK
49
+ const sdk = new sdk_node_1.NodeSDK({
50
+ resource: new resources_1.Resource({
51
+ [semantic_conventions_1.SemanticResourceAttributes.SERVICE_NAME]: serviceName,
52
+ [semantic_conventions_1.SemanticResourceAttributes.SERVICE_VERSION]: serviceVersion,
53
+ [semantic_conventions_1.SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: env,
54
+ }),
55
+ traceExporter: new exporter_otlp_proto_1.OTLPTraceExporter({
56
+ url: otelTracesEndpoint,
57
+ }), // Type compatibility workaround for version mismatch
58
+ instrumentations: [
59
+ (0, auto_instrumentations_node_1.getNodeAutoInstrumentations)({
60
+ // Disable fs instrumentation in production for performance
61
+ "@opentelemetry/instrumentation-fs": {
62
+ enabled: environment !== "production",
63
+ },
64
+ // Enable HTTP instrumentation (automatic)
65
+ "@opentelemetry/instrumentation-http": {
66
+ enabled: true,
67
+ },
68
+ }),
69
+ ],
70
+ });
71
+ sdk.start();
72
+ isInitialized = true;
73
+ global.__swirepayOtelSdk = sdk;
74
+ // Create an initial span so there's always an active span available
75
+ const { trace } = require("@opentelemetry/api");
76
+ const tracer = trace.getTracer(serviceName, serviceVersion);
77
+ const span = tracer.startSpan("swirepay-logger-init");
78
+ span.setAttribute("init", true);
79
+ span.end();
80
+ // Graceful shutdown
81
+ process.on("SIGTERM", () => {
82
+ sdk.shutdown().finally(() => process.exit(0));
83
+ });
84
+ return sdk;
85
+ }
86
+ catch (error) {
87
+ throw new Error(`Failed to initialize OpenTelemetry: ${error instanceof Error ? error.message : String(error)}`);
88
+ }
89
+ }
@@ -0,0 +1,18 @@
1
+ import Transport from 'winston-transport';
2
+ import { LogEntry } from 'winston';
3
+ /**
4
+ * OpenTelemetry Transport for Winston
5
+ * Automatically sends logs to OpenTelemetry when initialized
6
+ */
7
+ export declare class OtelTransport extends Transport {
8
+ private logger;
9
+ private environment;
10
+ constructor(opts: {
11
+ environment: string;
12
+ serviceName: string;
13
+ serviceVersion: string;
14
+ });
15
+ log(info: LogEntry, callback: () => void): void;
16
+ private mapLogLevel;
17
+ private getSeverityNumber;
18
+ }
@@ -0,0 +1,125 @@
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.OtelTransport = void 0;
7
+ const winston_transport_1 = __importDefault(require("winston-transport"));
8
+ const api_logs_1 = require("@opentelemetry/api-logs");
9
+ /**
10
+ * OpenTelemetry Transport for Winston
11
+ * Automatically sends logs to OpenTelemetry when initialized
12
+ */
13
+ class OtelTransport extends winston_transport_1.default {
14
+ constructor(opts) {
15
+ super();
16
+ this.environment = opts.environment;
17
+ // OpenTelemetry is REQUIRED - get logger from OpenTelemetry API
18
+ try {
19
+ const loggerProvider = api_logs_1.logs.getLoggerProvider();
20
+ if (!loggerProvider) {
21
+ throw new Error('OpenTelemetry LoggerProvider is not available. Please initialize OpenTelemetry first.');
22
+ }
23
+ this.logger = loggerProvider.getLogger(opts.serviceName, opts.serviceVersion);
24
+ if (!this.logger) {
25
+ throw new Error('Failed to get OpenTelemetry logger. Please ensure OpenTelemetry is properly initialized.');
26
+ }
27
+ }
28
+ catch (error) {
29
+ // OpenTelemetry is required - throw error instead of silently failing
30
+ throw new Error(`OpenTelemetry is required for SwirepayLogger. ${error instanceof Error ? error.message : 'Please initialize OpenTelemetry before using the logger.'}`);
31
+ }
32
+ }
33
+ log(info, callback) {
34
+ setImmediate(() => {
35
+ this.emit('logged', info);
36
+ });
37
+ // OpenTelemetry logger is required (checked in constructor)
38
+ if (!this.logger) {
39
+ callback();
40
+ return;
41
+ }
42
+ try {
43
+ const level = this.mapLogLevel(info.level || 'info');
44
+ const message = info.message || JSON.stringify(info);
45
+ // Build log record body
46
+ const body = {
47
+ stringValue: message,
48
+ };
49
+ // Build attributes
50
+ const attributes = {
51
+ 'log.level': level,
52
+ 'service.name': info.service || 'unknown',
53
+ 'service.version': info.version || 'unknown',
54
+ 'deployment.environment': info.environment || 'unknown',
55
+ };
56
+ // Add trace context if available
57
+ if (info.traceId) {
58
+ attributes['trace_id'] = info.traceId;
59
+ }
60
+ if (info.spanId) {
61
+ attributes['span_id'] = info.spanId;
62
+ }
63
+ // Add custom attributes
64
+ if (info.attributes) {
65
+ Object.assign(attributes, info.attributes);
66
+ }
67
+ // Add specific fields based on log type
68
+ if (info.apiCall) {
69
+ attributes['api_call.method'] = info.apiCall.method;
70
+ attributes['api_call.url'] = info.apiCall.url;
71
+ attributes['api_call.status_code'] = info.apiCall.statusCode;
72
+ if (info.apiCall.duration) {
73
+ attributes['api_call.duration_ms'] = info.apiCall.duration;
74
+ }
75
+ }
76
+ if (info.request) {
77
+ attributes['http.method'] = info.request.method;
78
+ attributes['http.route'] = info.request.path;
79
+ attributes['http.status_code'] = info.request.statusCode;
80
+ if (info.request.duration) {
81
+ attributes['http.duration_ms'] = info.request.duration;
82
+ }
83
+ }
84
+ if (info.webhook) {
85
+ attributes['webhook.source'] = info.webhook.source;
86
+ attributes['webhook.event'] = info.webhook.event;
87
+ attributes['webhook.status'] = info.webhook.status;
88
+ }
89
+ // Emit log record
90
+ this.logger.emit({
91
+ body,
92
+ severityNumber: this.getSeverityNumber(level),
93
+ severityText: level.toUpperCase(),
94
+ attributes,
95
+ timestamp: Date.now() * 1000000, // Convert to nanoseconds
96
+ });
97
+ }
98
+ catch (error) {
99
+ // Silently fail - don't break logging if OpenTelemetry has issues
100
+ console.error('Error sending log to OpenTelemetry:', error);
101
+ }
102
+ callback();
103
+ }
104
+ mapLogLevel(level) {
105
+ const levelMap = {
106
+ error: 'error',
107
+ warn: 'warn',
108
+ info: 'info',
109
+ debug: 'debug',
110
+ };
111
+ return levelMap[level.toLowerCase()] || 'info';
112
+ }
113
+ getSeverityNumber(level) {
114
+ const severityMap = {
115
+ trace: 1,
116
+ debug: 5,
117
+ info: 9,
118
+ warn: 13,
119
+ error: 17,
120
+ fatal: 21,
121
+ };
122
+ return severityMap[level.toLowerCase()] || 9; // Default to INFO
123
+ }
124
+ }
125
+ exports.OtelTransport = OtelTransport;
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@swirepay-developer/common-logging-nodejs",
3
+ "version": "1.0.0",
4
+ "description": "Swirepay Common Logging SDK for Node.js",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://bitbucket.org/zetametrics/open_telementry.git",
14
+ "directory": "nodejs"
15
+ },
16
+ "engines": {
17
+ "node": ">=14.0.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "prepublishOnly": "npm run build",
22
+ "example": "npm run build && cd examples && node express-with-otel.js",
23
+ "start:example": "npm run build && cd examples && node express-with-otel.js",
24
+ "example:ts": "npm run build && node -r ts-node/register examples/with-opentelemetry.ts"
25
+ },
26
+ "dependencies": {
27
+ "@opentelemetry/api": "^1.8.0",
28
+ "@opentelemetry/api-logs": "^0.49.1",
29
+ "@opentelemetry/auto-instrumentations-node": "^0.67.3",
30
+ "@opentelemetry/exporter-logs-otlp-proto": "^0.208.0",
31
+ "@opentelemetry/exporter-otlp-proto": "^0.26.0",
32
+ "@opentelemetry/instrumentation": "^0.49.1",
33
+ "@opentelemetry/instrumentation-http": "^0.49.1",
34
+ "@opentelemetry/resources": "^1.30.1",
35
+ "@opentelemetry/sdk-logs": "^0.49.1",
36
+ "@opentelemetry/sdk-node": "^0.49.1",
37
+ "@opentelemetry/semantic-conventions": "^1.38.0",
38
+ "winston": "^3.11.0",
39
+ "winston-transport": "^4.7.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20.19.27",
43
+ "typescript": "^5.3.3"
44
+ },
45
+ "keywords": [
46
+ "logging",
47
+ "opentelemetry",
48
+ "loki",
49
+ "swirepay",
50
+ "nodejs"
51
+ ],
52
+ "author": "Swirepay",
53
+ "license": "UNLICENSED"
54
+ }