@payloops/observability 0.0.1
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/.env.example +4 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/publish.yml +40 -0
- package/README.md +152 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +505 -0
- package/package.json +36 -0
- package/src/index.ts +56 -0
- package/src/lib/context.ts +90 -0
- package/src/lib/logger.ts +77 -0
- package/src/lib/metrics.ts +146 -0
- package/src/lib/otel.ts +75 -0
- package/tsconfig.json +16 -0
package/.env.example
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: pnpm/action-setup@v4
|
|
17
|
+
with:
|
|
18
|
+
version: 9
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: '22'
|
|
23
|
+
cache: 'pnpm'
|
|
24
|
+
|
|
25
|
+
- name: Update npm to latest
|
|
26
|
+
run: npm install -g npm@latest
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: pnpm install
|
|
30
|
+
|
|
31
|
+
- name: Type check
|
|
32
|
+
run: pnpm typecheck
|
|
33
|
+
|
|
34
|
+
- name: Lint
|
|
35
|
+
run: pnpm lint
|
|
36
|
+
|
|
37
|
+
- name: Build
|
|
38
|
+
run: pnpm build
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: pnpm/action-setup@v4
|
|
19
|
+
with:
|
|
20
|
+
version: 9
|
|
21
|
+
|
|
22
|
+
- uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '22'
|
|
25
|
+
cache: 'pnpm'
|
|
26
|
+
registry-url: 'https://registry.npmjs.org'
|
|
27
|
+
|
|
28
|
+
- name: Update npm to latest
|
|
29
|
+
run: npm install -g npm@latest
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: pnpm install
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: pnpm build
|
|
36
|
+
|
|
37
|
+
- name: Publish to npm with provenance
|
|
38
|
+
run: npm publish --access public --provenance
|
|
39
|
+
env:
|
|
40
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @payloops/observability
|
|
2
|
+
|
|
3
|
+
Shared observability package for PayLoops services. Provides OpenTelemetry tracing, structured logging, correlation context, and metrics.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @payloops/observability
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **OpenTelemetry** - Automatic tracing and metrics export
|
|
14
|
+
- **Structured Logging** - Pino logger with trace context
|
|
15
|
+
- **Correlation Context** - AsyncLocalStorage for request tracking
|
|
16
|
+
- **Metrics** - Pre-defined counters and histograms for payments, webhooks, HTTP, and workflows
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
### Initialize Telemetry
|
|
21
|
+
|
|
22
|
+
Initialize OpenTelemetry **before any other imports** in your entry point:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { initTelemetry } from '@payloops/observability';
|
|
26
|
+
|
|
27
|
+
// Simple initialization
|
|
28
|
+
initTelemetry('my-service', '1.0.0');
|
|
29
|
+
|
|
30
|
+
// Or with full config
|
|
31
|
+
initTelemetry({
|
|
32
|
+
serviceName: 'my-service',
|
|
33
|
+
serviceVersion: '1.0.0',
|
|
34
|
+
otlpEndpoint: 'http://localhost:4318',
|
|
35
|
+
environment: 'production',
|
|
36
|
+
enabledInstrumentations: {
|
|
37
|
+
http: true,
|
|
38
|
+
pg: true,
|
|
39
|
+
fs: false
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Logging
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { logger, createRequestLogger } from '@payloops/observability';
|
|
48
|
+
|
|
49
|
+
// Basic logging (automatically includes trace context)
|
|
50
|
+
logger.info({ orderId: '123' }, 'Processing order');
|
|
51
|
+
|
|
52
|
+
// Create a request-scoped logger
|
|
53
|
+
const reqLogger = createRequestLogger('req-123', 'POST', '/orders');
|
|
54
|
+
reqLogger.info('Request started');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Correlation Context
|
|
58
|
+
|
|
59
|
+
Track requests across async boundaries:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import {
|
|
63
|
+
withCorrelationContext,
|
|
64
|
+
getCorrelationContext,
|
|
65
|
+
extractCorrelationId,
|
|
66
|
+
createPropagationHeaders
|
|
67
|
+
} from '@payloops/observability';
|
|
68
|
+
|
|
69
|
+
// In HTTP middleware
|
|
70
|
+
app.use((req, res, next) => {
|
|
71
|
+
const correlationId = extractCorrelationId(req.headers);
|
|
72
|
+
|
|
73
|
+
withCorrelationContext({ correlationId }, () => {
|
|
74
|
+
// All code in this context has access to correlationId
|
|
75
|
+
next();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Later in your code
|
|
80
|
+
const ctx = getCorrelationContext();
|
|
81
|
+
console.log(ctx?.correlationId); // Available anywhere in the call stack
|
|
82
|
+
|
|
83
|
+
// When calling downstream services
|
|
84
|
+
const headers = createPropagationHeaders(ctx.correlationId);
|
|
85
|
+
fetch('http://other-service/api', { headers });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Metrics
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import {
|
|
92
|
+
recordPaymentAttempt,
|
|
93
|
+
recordPaymentAmount,
|
|
94
|
+
recordHttpRequest,
|
|
95
|
+
recordWorkflowStarted
|
|
96
|
+
} from '@payloops/observability';
|
|
97
|
+
|
|
98
|
+
// Record a payment attempt
|
|
99
|
+
recordPaymentAttempt('stripe', 'USD', 'success');
|
|
100
|
+
recordPaymentAmount(9999, 'stripe', 'USD');
|
|
101
|
+
|
|
102
|
+
// Record HTTP request
|
|
103
|
+
recordHttpRequest('POST', '/v1/orders', 201, 45.2);
|
|
104
|
+
|
|
105
|
+
// Record workflow execution
|
|
106
|
+
recordWorkflowStarted('PaymentWorkflow', 'stripe-payments');
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Available Exports
|
|
110
|
+
|
|
111
|
+
### OpenTelemetry
|
|
112
|
+
- `initTelemetry(config)` - Initialize OpenTelemetry SDK
|
|
113
|
+
- `shutdownTelemetry()` - Gracefully shutdown telemetry
|
|
114
|
+
|
|
115
|
+
### Logger
|
|
116
|
+
- `logger` - Base Pino logger with trace context
|
|
117
|
+
- `createActivityLogger(name, correlationId)` - Logger for Temporal activities
|
|
118
|
+
- `createWorkflowLogger(workflowId, correlationId)` - Logger for workflows
|
|
119
|
+
- `createRequestLogger(requestId, method, path)` - Logger for HTTP requests
|
|
120
|
+
|
|
121
|
+
### Correlation Context
|
|
122
|
+
- `getCorrelationContext()` - Get current context
|
|
123
|
+
- `withCorrelationContext(ctx, fn)` - Run function with context
|
|
124
|
+
- `generateCorrelationId()` - Generate new ID
|
|
125
|
+
- `extractCorrelationId(headers)` - Extract from HTTP headers
|
|
126
|
+
- `createPropagationHeaders(correlationId)` - Create headers for downstream calls
|
|
127
|
+
- `CORRELATION_ID_HEADER` - Header name constant
|
|
128
|
+
|
|
129
|
+
### Metrics
|
|
130
|
+
| Metric | Type | Description |
|
|
131
|
+
|--------|------|-------------|
|
|
132
|
+
| `payments_total` | Counter | Payment attempts |
|
|
133
|
+
| `payment_amount` | Histogram | Payment amounts |
|
|
134
|
+
| `payment_latency_ms` | Histogram | Payment processing time |
|
|
135
|
+
| `webhook_deliveries_total` | Counter | Webhook delivery attempts |
|
|
136
|
+
| `webhook_latency_ms` | Histogram | Webhook delivery time |
|
|
137
|
+
| `http_requests_total` | Counter | HTTP requests |
|
|
138
|
+
| `http_request_latency_ms` | Histogram | HTTP request time |
|
|
139
|
+
| `http_active_requests` | Gauge | Active HTTP requests |
|
|
140
|
+
| `db_query_duration_ms` | Histogram | Database query time |
|
|
141
|
+
| `workflow_started_total` | Counter | Workflows started |
|
|
142
|
+
| `workflow_completed_total` | Counter | Workflows completed |
|
|
143
|
+
| `workflow_failed_total` | Counter | Workflows failed |
|
|
144
|
+
| `activity_latency_ms` | Histogram | Activity execution time |
|
|
145
|
+
|
|
146
|
+
## Environment Variables
|
|
147
|
+
|
|
148
|
+
| Variable | Description | Default |
|
|
149
|
+
|----------|-------------|---------|
|
|
150
|
+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP endpoint | `http://localhost:4318` |
|
|
151
|
+
| `OTEL_SERVICE_NAME` | Service name | `loop` |
|
|
152
|
+
| `NODE_ENV` | Environment | `development` |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
2
|
+
import pino from 'pino';
|
|
3
|
+
import * as _opentelemetry_api from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
interface TelemetryConfig {
|
|
6
|
+
serviceName: string;
|
|
7
|
+
serviceVersion?: string;
|
|
8
|
+
otlpEndpoint?: string;
|
|
9
|
+
environment?: string;
|
|
10
|
+
enabledInstrumentations?: {
|
|
11
|
+
fs?: boolean;
|
|
12
|
+
http?: boolean;
|
|
13
|
+
pg?: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
declare function initTelemetry(config: TelemetryConfig | string, serviceVersion?: string): NodeSDK;
|
|
17
|
+
declare function shutdownTelemetry(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Base logger with trace context mixin
|
|
21
|
+
*/
|
|
22
|
+
declare const logger: pino.Logger<never, boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Create a child logger for a specific activity
|
|
25
|
+
*/
|
|
26
|
+
declare function createActivityLogger(activityName: string, correlationId?: string): pino.Logger<never, boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Create a child logger for a specific workflow
|
|
29
|
+
*/
|
|
30
|
+
declare function createWorkflowLogger(workflowId: string, correlationId?: string): pino.Logger<never, boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Create a child logger for HTTP requests
|
|
33
|
+
*/
|
|
34
|
+
declare function createRequestLogger(requestId: string, method: string, path: string): pino.Logger<never, boolean>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Correlation context for tracking requests across services
|
|
38
|
+
*/
|
|
39
|
+
interface CorrelationContext {
|
|
40
|
+
correlationId: string;
|
|
41
|
+
merchantId?: string;
|
|
42
|
+
orderId?: string;
|
|
43
|
+
workflowId?: string;
|
|
44
|
+
}
|
|
45
|
+
declare const CORRELATION_ID_HEADER = "X-Correlation-ID";
|
|
46
|
+
declare const REQUEST_ID_HEADER = "X-Request-ID";
|
|
47
|
+
/**
|
|
48
|
+
* Get the current correlation context
|
|
49
|
+
*/
|
|
50
|
+
declare function getCorrelationContext(): CorrelationContext | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Run a function with a correlation context
|
|
53
|
+
*/
|
|
54
|
+
declare function withCorrelationContext<T>(ctx: CorrelationContext, fn: () => T): T;
|
|
55
|
+
/**
|
|
56
|
+
* Run an async function with a correlation context
|
|
57
|
+
*/
|
|
58
|
+
declare function withCorrelationContextAsync<T>(ctx: CorrelationContext, fn: () => Promise<T>): Promise<T>;
|
|
59
|
+
/**
|
|
60
|
+
* Generate a new correlation ID
|
|
61
|
+
*/
|
|
62
|
+
declare function generateCorrelationId(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Extract correlation ID from headers (case-insensitive)
|
|
65
|
+
*/
|
|
66
|
+
declare function extractCorrelationId(headers: Record<string, string | undefined>): string;
|
|
67
|
+
/**
|
|
68
|
+
* Create headers for propagating correlation context to downstream services
|
|
69
|
+
*/
|
|
70
|
+
declare function createPropagationHeaders(correlationId: string): Record<string, string>;
|
|
71
|
+
/**
|
|
72
|
+
* Create correlation context from Temporal workflow memo
|
|
73
|
+
*/
|
|
74
|
+
declare function createContextFromMemo(memo: Record<string, unknown>): CorrelationContext;
|
|
75
|
+
|
|
76
|
+
declare const paymentCounter: _opentelemetry_api.Counter<_opentelemetry_api.Attributes>;
|
|
77
|
+
declare const paymentAmountHistogram: _opentelemetry_api.Histogram<_opentelemetry_api.Attributes>;
|
|
78
|
+
declare const paymentLatencyHistogram: _opentelemetry_api.Histogram<_opentelemetry_api.Attributes>;
|
|
79
|
+
declare const webhookDeliveryCounter: _opentelemetry_api.Counter<_opentelemetry_api.Attributes>;
|
|
80
|
+
declare const webhookLatencyHistogram: _opentelemetry_api.Histogram<_opentelemetry_api.Attributes>;
|
|
81
|
+
declare const httpRequestCounter: _opentelemetry_api.Counter<_opentelemetry_api.Attributes>;
|
|
82
|
+
declare const httpRequestLatencyHistogram: _opentelemetry_api.Histogram<_opentelemetry_api.Attributes>;
|
|
83
|
+
declare const activeRequestsGauge: _opentelemetry_api.UpDownCounter<_opentelemetry_api.Attributes>;
|
|
84
|
+
declare const dbQueryHistogram: _opentelemetry_api.Histogram<_opentelemetry_api.Attributes>;
|
|
85
|
+
declare const dbConnectionGauge: _opentelemetry_api.UpDownCounter<_opentelemetry_api.Attributes>;
|
|
86
|
+
declare const workflowStartedCounter: _opentelemetry_api.Counter<_opentelemetry_api.Attributes>;
|
|
87
|
+
declare const workflowCompletedCounter: _opentelemetry_api.Counter<_opentelemetry_api.Attributes>;
|
|
88
|
+
declare const workflowFailedCounter: _opentelemetry_api.Counter<_opentelemetry_api.Attributes>;
|
|
89
|
+
declare const activityLatencyHistogram: _opentelemetry_api.Histogram<_opentelemetry_api.Attributes>;
|
|
90
|
+
declare function recordPaymentAttempt(processor: string, currency: string, status: 'success' | 'failed' | 'pending'): void;
|
|
91
|
+
declare function recordPaymentAmount(amount: number, processor: string, currency: string): void;
|
|
92
|
+
declare function recordPaymentLatency(durationMs: number, processor: string, status: 'success' | 'failed'): void;
|
|
93
|
+
declare function recordWebhookDelivery(status: 'success' | 'failed', attempt: number): void;
|
|
94
|
+
declare function recordWebhookLatency(durationMs: number, status: 'success' | 'failed'): void;
|
|
95
|
+
declare function recordHttpRequest(method: string, path: string, statusCode: number, durationMs: number): void;
|
|
96
|
+
declare function recordWorkflowStarted(workflowType: string, taskQueue: string): void;
|
|
97
|
+
declare function recordWorkflowCompleted(workflowType: string, taskQueue: string, durationMs: number): void;
|
|
98
|
+
declare function recordWorkflowFailed(workflowType: string, taskQueue: string, errorType: string): void;
|
|
99
|
+
declare function recordActivityLatency(activityType: string, durationMs: number, status: 'success' | 'failed'): void;
|
|
100
|
+
|
|
101
|
+
export { CORRELATION_ID_HEADER, type CorrelationContext, REQUEST_ID_HEADER, type TelemetryConfig, activeRequestsGauge, activityLatencyHistogram, createActivityLogger, createContextFromMemo, createPropagationHeaders, createRequestLogger, createWorkflowLogger, dbConnectionGauge, dbQueryHistogram, extractCorrelationId, generateCorrelationId, getCorrelationContext, httpRequestCounter, httpRequestLatencyHistogram, initTelemetry, logger, paymentAmountHistogram, paymentCounter, paymentLatencyHistogram, recordActivityLatency, recordHttpRequest, recordPaymentAmount, recordPaymentAttempt, recordPaymentLatency, recordWebhookDelivery, recordWebhookLatency, recordWorkflowCompleted, recordWorkflowFailed, recordWorkflowStarted, shutdownTelemetry, webhookDeliveryCounter, webhookLatencyHistogram, withCorrelationContext, withCorrelationContextAsync, workflowCompletedCounter, workflowFailedCounter, workflowStartedCounter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
// src/lib/otel.ts
|
|
2
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
3
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
4
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
5
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
6
|
+
|
|
7
|
+
// node_modules/@opentelemetry/resources/build/esm/Resource.js
|
|
8
|
+
import { diag } from "@opentelemetry/api";
|
|
9
|
+
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_TELEMETRY_SDK_LANGUAGE as SEMRESATTRS_TELEMETRY_SDK_LANGUAGE2, SEMRESATTRS_TELEMETRY_SDK_NAME as SEMRESATTRS_TELEMETRY_SDK_NAME2, SEMRESATTRS_TELEMETRY_SDK_VERSION as SEMRESATTRS_TELEMETRY_SDK_VERSION2 } from "@opentelemetry/semantic-conventions";
|
|
10
|
+
|
|
11
|
+
// node_modules/@opentelemetry/core/build/esm/version.js
|
|
12
|
+
var VERSION = "1.30.1";
|
|
13
|
+
|
|
14
|
+
// node_modules/@opentelemetry/core/build/esm/platform/node/sdk-info.js
|
|
15
|
+
import { SEMRESATTRS_TELEMETRY_SDK_NAME, SEMRESATTRS_PROCESS_RUNTIME_NAME, SEMRESATTRS_TELEMETRY_SDK_LANGUAGE, TELEMETRYSDKLANGUAGEVALUES_NODEJS, SEMRESATTRS_TELEMETRY_SDK_VERSION } from "@opentelemetry/semantic-conventions";
|
|
16
|
+
var _a;
|
|
17
|
+
var SDK_INFO = (_a = {}, _a[SEMRESATTRS_TELEMETRY_SDK_NAME] = "opentelemetry", _a[SEMRESATTRS_PROCESS_RUNTIME_NAME] = "node", _a[SEMRESATTRS_TELEMETRY_SDK_LANGUAGE] = TELEMETRYSDKLANGUAGEVALUES_NODEJS, _a[SEMRESATTRS_TELEMETRY_SDK_VERSION] = VERSION, _a);
|
|
18
|
+
|
|
19
|
+
// node_modules/@opentelemetry/resources/build/esm/platform/node/default-service-name.js
|
|
20
|
+
function defaultServiceName() {
|
|
21
|
+
return "unknown_service:" + process.argv0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// node_modules/@opentelemetry/resources/build/esm/Resource.js
|
|
25
|
+
var __assign = function() {
|
|
26
|
+
__assign = Object.assign || function(t) {
|
|
27
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
28
|
+
s = arguments[i];
|
|
29
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
30
|
+
t[p] = s[p];
|
|
31
|
+
}
|
|
32
|
+
return t;
|
|
33
|
+
};
|
|
34
|
+
return __assign.apply(this, arguments);
|
|
35
|
+
};
|
|
36
|
+
var __awaiter = function(thisArg, _arguments, P, generator) {
|
|
37
|
+
function adopt(value) {
|
|
38
|
+
return value instanceof P ? value : new P(function(resolve) {
|
|
39
|
+
resolve(value);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return new (P || (P = Promise))(function(resolve, reject) {
|
|
43
|
+
function fulfilled(value) {
|
|
44
|
+
try {
|
|
45
|
+
step(generator.next(value));
|
|
46
|
+
} catch (e) {
|
|
47
|
+
reject(e);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function rejected(value) {
|
|
51
|
+
try {
|
|
52
|
+
step(generator["throw"](value));
|
|
53
|
+
} catch (e) {
|
|
54
|
+
reject(e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function step(result) {
|
|
58
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
59
|
+
}
|
|
60
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
var __generator = function(thisArg, body) {
|
|
64
|
+
var _ = { label: 0, sent: function() {
|
|
65
|
+
if (t[0] & 1) throw t[1];
|
|
66
|
+
return t[1];
|
|
67
|
+
}, trys: [], ops: [] }, f, y, t, g;
|
|
68
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() {
|
|
69
|
+
return this;
|
|
70
|
+
}), g;
|
|
71
|
+
function verb(n) {
|
|
72
|
+
return function(v) {
|
|
73
|
+
return step([n, v]);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function step(op) {
|
|
77
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
78
|
+
while (_) try {
|
|
79
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
80
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
81
|
+
switch (op[0]) {
|
|
82
|
+
case 0:
|
|
83
|
+
case 1:
|
|
84
|
+
t = op;
|
|
85
|
+
break;
|
|
86
|
+
case 4:
|
|
87
|
+
_.label++;
|
|
88
|
+
return { value: op[1], done: false };
|
|
89
|
+
case 5:
|
|
90
|
+
_.label++;
|
|
91
|
+
y = op[1];
|
|
92
|
+
op = [0];
|
|
93
|
+
continue;
|
|
94
|
+
case 7:
|
|
95
|
+
op = _.ops.pop();
|
|
96
|
+
_.trys.pop();
|
|
97
|
+
continue;
|
|
98
|
+
default:
|
|
99
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
|
|
100
|
+
_ = 0;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
|
|
104
|
+
_.label = op[1];
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
if (op[0] === 6 && _.label < t[1]) {
|
|
108
|
+
_.label = t[1];
|
|
109
|
+
t = op;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (t && _.label < t[2]) {
|
|
113
|
+
_.label = t[2];
|
|
114
|
+
_.ops.push(op);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
if (t[2]) _.ops.pop();
|
|
118
|
+
_.trys.pop();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
op = body.call(thisArg, _);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
op = [6, e];
|
|
124
|
+
y = 0;
|
|
125
|
+
} finally {
|
|
126
|
+
f = t = 0;
|
|
127
|
+
}
|
|
128
|
+
if (op[0] & 5) throw op[1];
|
|
129
|
+
return { value: op[0] ? op[1] : void 0, done: true };
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var __read = function(o, n) {
|
|
133
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
134
|
+
if (!m) return o;
|
|
135
|
+
var i = m.call(o), r, ar = [], e;
|
|
136
|
+
try {
|
|
137
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
e = { error };
|
|
140
|
+
} finally {
|
|
141
|
+
try {
|
|
142
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
143
|
+
} finally {
|
|
144
|
+
if (e) throw e.error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return ar;
|
|
148
|
+
};
|
|
149
|
+
var Resource = (
|
|
150
|
+
/** @class */
|
|
151
|
+
(function() {
|
|
152
|
+
function Resource2(attributes, asyncAttributesPromise) {
|
|
153
|
+
var _this = this;
|
|
154
|
+
var _a2;
|
|
155
|
+
this._attributes = attributes;
|
|
156
|
+
this.asyncAttributesPending = asyncAttributesPromise != null;
|
|
157
|
+
this._syncAttributes = (_a2 = this._attributes) !== null && _a2 !== void 0 ? _a2 : {};
|
|
158
|
+
this._asyncAttributesPromise = asyncAttributesPromise === null || asyncAttributesPromise === void 0 ? void 0 : asyncAttributesPromise.then(function(asyncAttributes) {
|
|
159
|
+
_this._attributes = Object.assign({}, _this._attributes, asyncAttributes);
|
|
160
|
+
_this.asyncAttributesPending = false;
|
|
161
|
+
return asyncAttributes;
|
|
162
|
+
}, function(err) {
|
|
163
|
+
diag.debug("a resource's async attributes promise rejected: %s", err);
|
|
164
|
+
_this.asyncAttributesPending = false;
|
|
165
|
+
return {};
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
Resource2.empty = function() {
|
|
169
|
+
return Resource2.EMPTY;
|
|
170
|
+
};
|
|
171
|
+
Resource2.default = function() {
|
|
172
|
+
var _a2;
|
|
173
|
+
return new Resource2((_a2 = {}, _a2[SEMRESATTRS_SERVICE_NAME] = defaultServiceName(), _a2[SEMRESATTRS_TELEMETRY_SDK_LANGUAGE2] = SDK_INFO[SEMRESATTRS_TELEMETRY_SDK_LANGUAGE2], _a2[SEMRESATTRS_TELEMETRY_SDK_NAME2] = SDK_INFO[SEMRESATTRS_TELEMETRY_SDK_NAME2], _a2[SEMRESATTRS_TELEMETRY_SDK_VERSION2] = SDK_INFO[SEMRESATTRS_TELEMETRY_SDK_VERSION2], _a2));
|
|
174
|
+
};
|
|
175
|
+
Object.defineProperty(Resource2.prototype, "attributes", {
|
|
176
|
+
get: function() {
|
|
177
|
+
var _a2;
|
|
178
|
+
if (this.asyncAttributesPending) {
|
|
179
|
+
diag.error("Accessing resource attributes before async attributes settled");
|
|
180
|
+
}
|
|
181
|
+
return (_a2 = this._attributes) !== null && _a2 !== void 0 ? _a2 : {};
|
|
182
|
+
},
|
|
183
|
+
enumerable: false,
|
|
184
|
+
configurable: true
|
|
185
|
+
});
|
|
186
|
+
Resource2.prototype.waitForAsyncAttributes = function() {
|
|
187
|
+
return __awaiter(this, void 0, void 0, function() {
|
|
188
|
+
return __generator(this, function(_a2) {
|
|
189
|
+
switch (_a2.label) {
|
|
190
|
+
case 0:
|
|
191
|
+
if (!this.asyncAttributesPending) return [3, 2];
|
|
192
|
+
return [4, this._asyncAttributesPromise];
|
|
193
|
+
case 1:
|
|
194
|
+
_a2.sent();
|
|
195
|
+
_a2.label = 2;
|
|
196
|
+
case 2:
|
|
197
|
+
return [
|
|
198
|
+
2
|
|
199
|
+
/*return*/
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
Resource2.prototype.merge = function(other) {
|
|
206
|
+
var _this = this;
|
|
207
|
+
var _a2;
|
|
208
|
+
if (!other)
|
|
209
|
+
return this;
|
|
210
|
+
var mergedSyncAttributes = __assign(__assign({}, this._syncAttributes), (_a2 = other._syncAttributes) !== null && _a2 !== void 0 ? _a2 : other.attributes);
|
|
211
|
+
if (!this._asyncAttributesPromise && !other._asyncAttributesPromise) {
|
|
212
|
+
return new Resource2(mergedSyncAttributes);
|
|
213
|
+
}
|
|
214
|
+
var mergedAttributesPromise = Promise.all([
|
|
215
|
+
this._asyncAttributesPromise,
|
|
216
|
+
other._asyncAttributesPromise
|
|
217
|
+
]).then(function(_a3) {
|
|
218
|
+
var _b;
|
|
219
|
+
var _c = __read(_a3, 2), thisAsyncAttributes = _c[0], otherAsyncAttributes = _c[1];
|
|
220
|
+
return __assign(__assign(__assign(__assign({}, _this._syncAttributes), thisAsyncAttributes), (_b = other._syncAttributes) !== null && _b !== void 0 ? _b : other.attributes), otherAsyncAttributes);
|
|
221
|
+
});
|
|
222
|
+
return new Resource2(mergedSyncAttributes, mergedAttributesPromise);
|
|
223
|
+
};
|
|
224
|
+
Resource2.EMPTY = new Resource2({});
|
|
225
|
+
return Resource2;
|
|
226
|
+
})()
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// src/lib/otel.ts
|
|
230
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
231
|
+
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
232
|
+
var sdk = null;
|
|
233
|
+
function initTelemetry(config, serviceVersion = "0.0.1") {
|
|
234
|
+
if (sdk) return sdk;
|
|
235
|
+
const cfg = typeof config === "string" ? { serviceName: config, serviceVersion } : config;
|
|
236
|
+
const otlpEndpoint = cfg.otlpEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
|
|
237
|
+
const environment = cfg.environment || process.env.NODE_ENV || "development";
|
|
238
|
+
sdk = new NodeSDK({
|
|
239
|
+
resource: new Resource({
|
|
240
|
+
[ATTR_SERVICE_NAME]: cfg.serviceName,
|
|
241
|
+
[ATTR_SERVICE_VERSION]: cfg.serviceVersion || "0.0.1",
|
|
242
|
+
"deployment.environment": environment
|
|
243
|
+
}),
|
|
244
|
+
traceExporter: new OTLPTraceExporter({
|
|
245
|
+
url: `${otlpEndpoint}/v1/traces`
|
|
246
|
+
}),
|
|
247
|
+
metricReader: new PeriodicExportingMetricReader({
|
|
248
|
+
exporter: new OTLPMetricExporter({
|
|
249
|
+
url: `${otlpEndpoint}/v1/metrics`
|
|
250
|
+
}),
|
|
251
|
+
exportIntervalMillis: 3e4
|
|
252
|
+
}),
|
|
253
|
+
instrumentations: [
|
|
254
|
+
getNodeAutoInstrumentations({
|
|
255
|
+
"@opentelemetry/instrumentation-fs": { enabled: cfg.enabledInstrumentations?.fs ?? false },
|
|
256
|
+
"@opentelemetry/instrumentation-http": { enabled: cfg.enabledInstrumentations?.http ?? true },
|
|
257
|
+
"@opentelemetry/instrumentation-pg": { enabled: cfg.enabledInstrumentations?.pg ?? true }
|
|
258
|
+
})
|
|
259
|
+
]
|
|
260
|
+
});
|
|
261
|
+
sdk.start();
|
|
262
|
+
process.on("SIGTERM", () => {
|
|
263
|
+
sdk?.shutdown().then(() => console.log("Telemetry shut down")).catch((err) => console.error("Telemetry shutdown error", err));
|
|
264
|
+
});
|
|
265
|
+
return sdk;
|
|
266
|
+
}
|
|
267
|
+
function shutdownTelemetry() {
|
|
268
|
+
if (!sdk) return Promise.resolve();
|
|
269
|
+
return sdk.shutdown();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/lib/logger.ts
|
|
273
|
+
import pino from "pino";
|
|
274
|
+
import { trace } from "@opentelemetry/api";
|
|
275
|
+
|
|
276
|
+
// src/lib/context.ts
|
|
277
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
278
|
+
import { nanoid } from "nanoid";
|
|
279
|
+
import { context, propagation } from "@opentelemetry/api";
|
|
280
|
+
var correlationStorage = new AsyncLocalStorage();
|
|
281
|
+
var CORRELATION_ID_HEADER = "X-Correlation-ID";
|
|
282
|
+
var REQUEST_ID_HEADER = "X-Request-ID";
|
|
283
|
+
function getCorrelationContext() {
|
|
284
|
+
return correlationStorage.getStore();
|
|
285
|
+
}
|
|
286
|
+
function withCorrelationContext(ctx, fn) {
|
|
287
|
+
return correlationStorage.run(ctx, fn);
|
|
288
|
+
}
|
|
289
|
+
async function withCorrelationContextAsync(ctx, fn) {
|
|
290
|
+
return correlationStorage.run(ctx, fn);
|
|
291
|
+
}
|
|
292
|
+
function generateCorrelationId() {
|
|
293
|
+
return nanoid(21);
|
|
294
|
+
}
|
|
295
|
+
function extractCorrelationId(headers) {
|
|
296
|
+
const normalizedHeaders = {};
|
|
297
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
298
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
299
|
+
}
|
|
300
|
+
return normalizedHeaders[CORRELATION_ID_HEADER.toLowerCase()] || normalizedHeaders[REQUEST_ID_HEADER.toLowerCase()] || generateCorrelationId();
|
|
301
|
+
}
|
|
302
|
+
function createPropagationHeaders(correlationId) {
|
|
303
|
+
const headers = {
|
|
304
|
+
[CORRELATION_ID_HEADER]: correlationId
|
|
305
|
+
};
|
|
306
|
+
propagation.inject(context.active(), headers);
|
|
307
|
+
return headers;
|
|
308
|
+
}
|
|
309
|
+
function createContextFromMemo(memo) {
|
|
310
|
+
return {
|
|
311
|
+
correlationId: memo.correlationId || generateCorrelationId(),
|
|
312
|
+
merchantId: memo.merchantId,
|
|
313
|
+
orderId: memo.orderId,
|
|
314
|
+
workflowId: memo.workflowId
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/lib/logger.ts
|
|
319
|
+
var NODE_ENV = process.env.NODE_ENV || "development";
|
|
320
|
+
var SERVICE_NAME = process.env.OTEL_SERVICE_NAME || "loop";
|
|
321
|
+
var traceMixin = () => {
|
|
322
|
+
const mixinData = {};
|
|
323
|
+
const span = trace.getActiveSpan();
|
|
324
|
+
if (span) {
|
|
325
|
+
const spanContext = span.spanContext();
|
|
326
|
+
mixinData.trace_id = spanContext.traceId;
|
|
327
|
+
mixinData.span_id = spanContext.spanId;
|
|
328
|
+
}
|
|
329
|
+
const correlationCtx = getCorrelationContext();
|
|
330
|
+
if (correlationCtx) {
|
|
331
|
+
mixinData.correlation_id = correlationCtx.correlationId;
|
|
332
|
+
if (correlationCtx.merchantId) mixinData.merchant_id = correlationCtx.merchantId;
|
|
333
|
+
if (correlationCtx.orderId) mixinData.order_id = correlationCtx.orderId;
|
|
334
|
+
if (correlationCtx.workflowId) mixinData.workflow_id = correlationCtx.workflowId;
|
|
335
|
+
}
|
|
336
|
+
return mixinData;
|
|
337
|
+
};
|
|
338
|
+
var logger = pino({
|
|
339
|
+
level: NODE_ENV === "production" ? "info" : "debug",
|
|
340
|
+
mixin: traceMixin,
|
|
341
|
+
base: {
|
|
342
|
+
service: SERVICE_NAME,
|
|
343
|
+
env: NODE_ENV
|
|
344
|
+
},
|
|
345
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
346
|
+
transport: NODE_ENV !== "production" ? { target: "pino-pretty", options: { colorize: true } } : void 0
|
|
347
|
+
});
|
|
348
|
+
function createActivityLogger(activityName, correlationId) {
|
|
349
|
+
return logger.child({
|
|
350
|
+
activity: activityName,
|
|
351
|
+
correlationId
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function createWorkflowLogger(workflowId, correlationId) {
|
|
355
|
+
return logger.child({
|
|
356
|
+
workflowId,
|
|
357
|
+
correlationId
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function createRequestLogger(requestId, method, path) {
|
|
361
|
+
return logger.child({
|
|
362
|
+
requestId,
|
|
363
|
+
method,
|
|
364
|
+
path
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/lib/metrics.ts
|
|
369
|
+
import { metrics, ValueType } from "@opentelemetry/api";
|
|
370
|
+
var meter = metrics.getMeter("payloops");
|
|
371
|
+
var paymentCounter = meter.createCounter("payments_total", {
|
|
372
|
+
description: "Total number of payment attempts",
|
|
373
|
+
valueType: ValueType.INT
|
|
374
|
+
});
|
|
375
|
+
var paymentAmountHistogram = meter.createHistogram("payment_amount", {
|
|
376
|
+
description: "Distribution of payment amounts",
|
|
377
|
+
unit: "cents",
|
|
378
|
+
valueType: ValueType.INT
|
|
379
|
+
});
|
|
380
|
+
var paymentLatencyHistogram = meter.createHistogram("payment_latency_ms", {
|
|
381
|
+
description: "Payment processing latency",
|
|
382
|
+
unit: "ms",
|
|
383
|
+
valueType: ValueType.DOUBLE
|
|
384
|
+
});
|
|
385
|
+
var webhookDeliveryCounter = meter.createCounter("webhook_deliveries_total", {
|
|
386
|
+
description: "Total webhook delivery attempts",
|
|
387
|
+
valueType: ValueType.INT
|
|
388
|
+
});
|
|
389
|
+
var webhookLatencyHistogram = meter.createHistogram("webhook_latency_ms", {
|
|
390
|
+
description: "Webhook delivery latency",
|
|
391
|
+
unit: "ms",
|
|
392
|
+
valueType: ValueType.DOUBLE
|
|
393
|
+
});
|
|
394
|
+
var httpRequestCounter = meter.createCounter("http_requests_total", {
|
|
395
|
+
description: "Total HTTP requests",
|
|
396
|
+
valueType: ValueType.INT
|
|
397
|
+
});
|
|
398
|
+
var httpRequestLatencyHistogram = meter.createHistogram("http_request_latency_ms", {
|
|
399
|
+
description: "HTTP request latency",
|
|
400
|
+
unit: "ms",
|
|
401
|
+
valueType: ValueType.DOUBLE
|
|
402
|
+
});
|
|
403
|
+
var activeRequestsGauge = meter.createUpDownCounter("http_active_requests", {
|
|
404
|
+
description: "Number of active HTTP requests",
|
|
405
|
+
valueType: ValueType.INT
|
|
406
|
+
});
|
|
407
|
+
var dbQueryHistogram = meter.createHistogram("db_query_duration_ms", {
|
|
408
|
+
description: "Database query duration",
|
|
409
|
+
unit: "ms",
|
|
410
|
+
valueType: ValueType.DOUBLE
|
|
411
|
+
});
|
|
412
|
+
var dbConnectionGauge = meter.createUpDownCounter("db_connections_active", {
|
|
413
|
+
description: "Number of active database connections",
|
|
414
|
+
valueType: ValueType.INT
|
|
415
|
+
});
|
|
416
|
+
var workflowStartedCounter = meter.createCounter("workflow_started_total", {
|
|
417
|
+
description: "Total workflows started",
|
|
418
|
+
valueType: ValueType.INT
|
|
419
|
+
});
|
|
420
|
+
var workflowCompletedCounter = meter.createCounter("workflow_completed_total", {
|
|
421
|
+
description: "Total workflows completed",
|
|
422
|
+
valueType: ValueType.INT
|
|
423
|
+
});
|
|
424
|
+
var workflowFailedCounter = meter.createCounter("workflow_failed_total", {
|
|
425
|
+
description: "Total workflows failed",
|
|
426
|
+
valueType: ValueType.INT
|
|
427
|
+
});
|
|
428
|
+
var activityLatencyHistogram = meter.createHistogram("activity_latency_ms", {
|
|
429
|
+
description: "Activity execution latency",
|
|
430
|
+
unit: "ms",
|
|
431
|
+
valueType: ValueType.DOUBLE
|
|
432
|
+
});
|
|
433
|
+
function recordPaymentAttempt(processor, currency, status) {
|
|
434
|
+
paymentCounter.add(1, { processor, currency, status });
|
|
435
|
+
}
|
|
436
|
+
function recordPaymentAmount(amount, processor, currency) {
|
|
437
|
+
paymentAmountHistogram.record(amount, { processor, currency });
|
|
438
|
+
}
|
|
439
|
+
function recordPaymentLatency(durationMs, processor, status) {
|
|
440
|
+
paymentLatencyHistogram.record(durationMs, { processor, status });
|
|
441
|
+
}
|
|
442
|
+
function recordWebhookDelivery(status, attempt) {
|
|
443
|
+
webhookDeliveryCounter.add(1, { status, attempt: String(attempt) });
|
|
444
|
+
}
|
|
445
|
+
function recordWebhookLatency(durationMs, status) {
|
|
446
|
+
webhookLatencyHistogram.record(durationMs, { status });
|
|
447
|
+
}
|
|
448
|
+
function recordHttpRequest(method, path, statusCode, durationMs) {
|
|
449
|
+
const statusClass = `${Math.floor(statusCode / 100)}xx`;
|
|
450
|
+
httpRequestCounter.add(1, { method, path, status_code: String(statusCode), status_class: statusClass });
|
|
451
|
+
httpRequestLatencyHistogram.record(durationMs, { method, path, status_class: statusClass });
|
|
452
|
+
}
|
|
453
|
+
function recordWorkflowStarted(workflowType, taskQueue) {
|
|
454
|
+
workflowStartedCounter.add(1, { workflow_type: workflowType, task_queue: taskQueue });
|
|
455
|
+
}
|
|
456
|
+
function recordWorkflowCompleted(workflowType, taskQueue, durationMs) {
|
|
457
|
+
workflowCompletedCounter.add(1, { workflow_type: workflowType, task_queue: taskQueue });
|
|
458
|
+
}
|
|
459
|
+
function recordWorkflowFailed(workflowType, taskQueue, errorType) {
|
|
460
|
+
workflowFailedCounter.add(1, { workflow_type: workflowType, task_queue: taskQueue, error_type: errorType });
|
|
461
|
+
}
|
|
462
|
+
function recordActivityLatency(activityType, durationMs, status) {
|
|
463
|
+
activityLatencyHistogram.record(durationMs, { activity_type: activityType, status });
|
|
464
|
+
}
|
|
465
|
+
export {
|
|
466
|
+
CORRELATION_ID_HEADER,
|
|
467
|
+
REQUEST_ID_HEADER,
|
|
468
|
+
activeRequestsGauge,
|
|
469
|
+
activityLatencyHistogram,
|
|
470
|
+
createActivityLogger,
|
|
471
|
+
createContextFromMemo,
|
|
472
|
+
createPropagationHeaders,
|
|
473
|
+
createRequestLogger,
|
|
474
|
+
createWorkflowLogger,
|
|
475
|
+
dbConnectionGauge,
|
|
476
|
+
dbQueryHistogram,
|
|
477
|
+
extractCorrelationId,
|
|
478
|
+
generateCorrelationId,
|
|
479
|
+
getCorrelationContext,
|
|
480
|
+
httpRequestCounter,
|
|
481
|
+
httpRequestLatencyHistogram,
|
|
482
|
+
initTelemetry,
|
|
483
|
+
logger,
|
|
484
|
+
paymentAmountHistogram,
|
|
485
|
+
paymentCounter,
|
|
486
|
+
paymentLatencyHistogram,
|
|
487
|
+
recordActivityLatency,
|
|
488
|
+
recordHttpRequest,
|
|
489
|
+
recordPaymentAmount,
|
|
490
|
+
recordPaymentAttempt,
|
|
491
|
+
recordPaymentLatency,
|
|
492
|
+
recordWebhookDelivery,
|
|
493
|
+
recordWebhookLatency,
|
|
494
|
+
recordWorkflowCompleted,
|
|
495
|
+
recordWorkflowFailed,
|
|
496
|
+
recordWorkflowStarted,
|
|
497
|
+
shutdownTelemetry,
|
|
498
|
+
webhookDeliveryCounter,
|
|
499
|
+
webhookLatencyHistogram,
|
|
500
|
+
withCorrelationContext,
|
|
501
|
+
withCorrelationContextAsync,
|
|
502
|
+
workflowCompletedCounter,
|
|
503
|
+
workflowFailedCounter,
|
|
504
|
+
workflowStartedCounter
|
|
505
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@payloops/observability",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
15
|
+
"lint": "eslint src/",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@opentelemetry/api": "^1.9.0",
|
|
20
|
+
"@opentelemetry/auto-instrumentations-node": "^0.53.0",
|
|
21
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.57.0",
|
|
22
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
|
|
23
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
24
|
+
"@opentelemetry/sdk-node": "^0.57.0",
|
|
25
|
+
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
26
|
+
"nanoid": "^5.0.9",
|
|
27
|
+
"pino": "^9.6.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"eslint": "^9.0.0",
|
|
32
|
+
"pino-pretty": "^13.0.0",
|
|
33
|
+
"tsup": "^8.3.5",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// OpenTelemetry
|
|
2
|
+
export { initTelemetry, shutdownTelemetry, type TelemetryConfig } from './lib/otel';
|
|
3
|
+
|
|
4
|
+
// Logger
|
|
5
|
+
export { logger, createActivityLogger, createWorkflowLogger, createRequestLogger } from './lib/logger';
|
|
6
|
+
|
|
7
|
+
// Correlation Context
|
|
8
|
+
export {
|
|
9
|
+
getCorrelationContext,
|
|
10
|
+
withCorrelationContext,
|
|
11
|
+
withCorrelationContextAsync,
|
|
12
|
+
generateCorrelationId,
|
|
13
|
+
extractCorrelationId,
|
|
14
|
+
createPropagationHeaders,
|
|
15
|
+
createContextFromMemo,
|
|
16
|
+
CORRELATION_ID_HEADER,
|
|
17
|
+
REQUEST_ID_HEADER,
|
|
18
|
+
type CorrelationContext
|
|
19
|
+
} from './lib/context';
|
|
20
|
+
|
|
21
|
+
// Metrics
|
|
22
|
+
export {
|
|
23
|
+
// Payment metrics
|
|
24
|
+
paymentCounter,
|
|
25
|
+
paymentAmountHistogram,
|
|
26
|
+
paymentLatencyHistogram,
|
|
27
|
+
recordPaymentAttempt,
|
|
28
|
+
recordPaymentAmount,
|
|
29
|
+
recordPaymentLatency,
|
|
30
|
+
|
|
31
|
+
// Webhook metrics
|
|
32
|
+
webhookDeliveryCounter,
|
|
33
|
+
webhookLatencyHistogram,
|
|
34
|
+
recordWebhookDelivery,
|
|
35
|
+
recordWebhookLatency,
|
|
36
|
+
|
|
37
|
+
// HTTP metrics
|
|
38
|
+
httpRequestCounter,
|
|
39
|
+
httpRequestLatencyHistogram,
|
|
40
|
+
activeRequestsGauge,
|
|
41
|
+
recordHttpRequest,
|
|
42
|
+
|
|
43
|
+
// Database metrics
|
|
44
|
+
dbQueryHistogram,
|
|
45
|
+
dbConnectionGauge,
|
|
46
|
+
|
|
47
|
+
// Workflow metrics
|
|
48
|
+
workflowStartedCounter,
|
|
49
|
+
workflowCompletedCounter,
|
|
50
|
+
workflowFailedCounter,
|
|
51
|
+
activityLatencyHistogram,
|
|
52
|
+
recordWorkflowStarted,
|
|
53
|
+
recordWorkflowCompleted,
|
|
54
|
+
recordWorkflowFailed,
|
|
55
|
+
recordActivityLatency
|
|
56
|
+
} from './lib/metrics';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { nanoid } from 'nanoid';
|
|
3
|
+
import { context, propagation } from '@opentelemetry/api';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Correlation context for tracking requests across services
|
|
7
|
+
*/
|
|
8
|
+
export interface CorrelationContext {
|
|
9
|
+
correlationId: string;
|
|
10
|
+
merchantId?: string;
|
|
11
|
+
orderId?: string;
|
|
12
|
+
workflowId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const correlationStorage = new AsyncLocalStorage<CorrelationContext>();
|
|
16
|
+
|
|
17
|
+
// Standard headers for correlation
|
|
18
|
+
export const CORRELATION_ID_HEADER = 'X-Correlation-ID';
|
|
19
|
+
export const REQUEST_ID_HEADER = 'X-Request-ID';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the current correlation context
|
|
23
|
+
*/
|
|
24
|
+
export function getCorrelationContext(): CorrelationContext | undefined {
|
|
25
|
+
return correlationStorage.getStore();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run a function with a correlation context
|
|
30
|
+
*/
|
|
31
|
+
export function withCorrelationContext<T>(ctx: CorrelationContext, fn: () => T): T {
|
|
32
|
+
return correlationStorage.run(ctx, fn);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run an async function with a correlation context
|
|
37
|
+
*/
|
|
38
|
+
export async function withCorrelationContextAsync<T>(ctx: CorrelationContext, fn: () => Promise<T>): Promise<T> {
|
|
39
|
+
return correlationStorage.run(ctx, fn);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a new correlation ID
|
|
44
|
+
*/
|
|
45
|
+
export function generateCorrelationId(): string {
|
|
46
|
+
return nanoid(21);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract correlation ID from headers (case-insensitive)
|
|
51
|
+
*/
|
|
52
|
+
export function extractCorrelationId(headers: Record<string, string | undefined>): string {
|
|
53
|
+
// Normalize header keys to lowercase for lookup
|
|
54
|
+
const normalizedHeaders: Record<string, string | undefined> = {};
|
|
55
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
56
|
+
normalizedHeaders[key.toLowerCase()] = value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
normalizedHeaders[CORRELATION_ID_HEADER.toLowerCase()] ||
|
|
61
|
+
normalizedHeaders[REQUEST_ID_HEADER.toLowerCase()] ||
|
|
62
|
+
generateCorrelationId()
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create headers for propagating correlation context to downstream services
|
|
68
|
+
*/
|
|
69
|
+
export function createPropagationHeaders(correlationId: string): Record<string, string> {
|
|
70
|
+
const headers: Record<string, string> = {
|
|
71
|
+
[CORRELATION_ID_HEADER]: correlationId
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Inject OpenTelemetry trace context
|
|
75
|
+
propagation.inject(context.active(), headers);
|
|
76
|
+
|
|
77
|
+
return headers;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create correlation context from Temporal workflow memo
|
|
82
|
+
*/
|
|
83
|
+
export function createContextFromMemo(memo: Record<string, unknown>): CorrelationContext {
|
|
84
|
+
return {
|
|
85
|
+
correlationId: (memo.correlationId as string) || generateCorrelationId(),
|
|
86
|
+
merchantId: memo.merchantId as string | undefined,
|
|
87
|
+
orderId: memo.orderId as string | undefined,
|
|
88
|
+
workflowId: memo.workflowId as string | undefined
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { trace } from '@opentelemetry/api';
|
|
3
|
+
import { getCorrelationContext } from './context';
|
|
4
|
+
|
|
5
|
+
const NODE_ENV = process.env.NODE_ENV || 'development';
|
|
6
|
+
const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || 'loop';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mixin that adds trace context to every log entry
|
|
10
|
+
*/
|
|
11
|
+
const traceMixin = () => {
|
|
12
|
+
const mixinData: Record<string, string | undefined> = {};
|
|
13
|
+
|
|
14
|
+
// Add OpenTelemetry trace context
|
|
15
|
+
const span = trace.getActiveSpan();
|
|
16
|
+
if (span) {
|
|
17
|
+
const spanContext = span.spanContext();
|
|
18
|
+
mixinData.trace_id = spanContext.traceId;
|
|
19
|
+
mixinData.span_id = spanContext.spanId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Add correlation context
|
|
23
|
+
const correlationCtx = getCorrelationContext();
|
|
24
|
+
if (correlationCtx) {
|
|
25
|
+
mixinData.correlation_id = correlationCtx.correlationId;
|
|
26
|
+
if (correlationCtx.merchantId) mixinData.merchant_id = correlationCtx.merchantId;
|
|
27
|
+
if (correlationCtx.orderId) mixinData.order_id = correlationCtx.orderId;
|
|
28
|
+
if (correlationCtx.workflowId) mixinData.workflow_id = correlationCtx.workflowId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return mixinData;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Base logger with trace context mixin
|
|
36
|
+
*/
|
|
37
|
+
export const logger = pino({
|
|
38
|
+
level: NODE_ENV === 'production' ? 'info' : 'debug',
|
|
39
|
+
mixin: traceMixin,
|
|
40
|
+
base: {
|
|
41
|
+
service: SERVICE_NAME,
|
|
42
|
+
env: NODE_ENV
|
|
43
|
+
},
|
|
44
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
45
|
+
transport: NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { colorize: true } } : undefined
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a child logger for a specific activity
|
|
50
|
+
*/
|
|
51
|
+
export function createActivityLogger(activityName: string, correlationId?: string) {
|
|
52
|
+
return logger.child({
|
|
53
|
+
activity: activityName,
|
|
54
|
+
correlationId
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a child logger for a specific workflow
|
|
60
|
+
*/
|
|
61
|
+
export function createWorkflowLogger(workflowId: string, correlationId?: string) {
|
|
62
|
+
return logger.child({
|
|
63
|
+
workflowId,
|
|
64
|
+
correlationId
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a child logger for HTTP requests
|
|
70
|
+
*/
|
|
71
|
+
export function createRequestLogger(requestId: string, method: string, path: string) {
|
|
72
|
+
return logger.child({
|
|
73
|
+
requestId,
|
|
74
|
+
method,
|
|
75
|
+
path
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { metrics, ValueType } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
// Create a meter for the application
|
|
4
|
+
const meter = metrics.getMeter('payloops');
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Payment Metrics
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
export const paymentCounter = meter.createCounter('payments_total', {
|
|
11
|
+
description: 'Total number of payment attempts',
|
|
12
|
+
valueType: ValueType.INT
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const paymentAmountHistogram = meter.createHistogram('payment_amount', {
|
|
16
|
+
description: 'Distribution of payment amounts',
|
|
17
|
+
unit: 'cents',
|
|
18
|
+
valueType: ValueType.INT
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const paymentLatencyHistogram = meter.createHistogram('payment_latency_ms', {
|
|
22
|
+
description: 'Payment processing latency',
|
|
23
|
+
unit: 'ms',
|
|
24
|
+
valueType: ValueType.DOUBLE
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Webhook Metrics
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export const webhookDeliveryCounter = meter.createCounter('webhook_deliveries_total', {
|
|
32
|
+
description: 'Total webhook delivery attempts',
|
|
33
|
+
valueType: ValueType.INT
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const webhookLatencyHistogram = meter.createHistogram('webhook_latency_ms', {
|
|
37
|
+
description: 'Webhook delivery latency',
|
|
38
|
+
unit: 'ms',
|
|
39
|
+
valueType: ValueType.DOUBLE
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// HTTP Metrics
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
export const httpRequestCounter = meter.createCounter('http_requests_total', {
|
|
47
|
+
description: 'Total HTTP requests',
|
|
48
|
+
valueType: ValueType.INT
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const httpRequestLatencyHistogram = meter.createHistogram('http_request_latency_ms', {
|
|
52
|
+
description: 'HTTP request latency',
|
|
53
|
+
unit: 'ms',
|
|
54
|
+
valueType: ValueType.DOUBLE
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const activeRequestsGauge = meter.createUpDownCounter('http_active_requests', {
|
|
58
|
+
description: 'Number of active HTTP requests',
|
|
59
|
+
valueType: ValueType.INT
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Database Metrics
|
|
64
|
+
// =============================================================================
|
|
65
|
+
|
|
66
|
+
export const dbQueryHistogram = meter.createHistogram('db_query_duration_ms', {
|
|
67
|
+
description: 'Database query duration',
|
|
68
|
+
unit: 'ms',
|
|
69
|
+
valueType: ValueType.DOUBLE
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const dbConnectionGauge = meter.createUpDownCounter('db_connections_active', {
|
|
73
|
+
description: 'Number of active database connections',
|
|
74
|
+
valueType: ValueType.INT
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Temporal Workflow Metrics
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
export const workflowStartedCounter = meter.createCounter('workflow_started_total', {
|
|
82
|
+
description: 'Total workflows started',
|
|
83
|
+
valueType: ValueType.INT
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const workflowCompletedCounter = meter.createCounter('workflow_completed_total', {
|
|
87
|
+
description: 'Total workflows completed',
|
|
88
|
+
valueType: ValueType.INT
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const workflowFailedCounter = meter.createCounter('workflow_failed_total', {
|
|
92
|
+
description: 'Total workflows failed',
|
|
93
|
+
valueType: ValueType.INT
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const activityLatencyHistogram = meter.createHistogram('activity_latency_ms', {
|
|
97
|
+
description: 'Activity execution latency',
|
|
98
|
+
unit: 'ms',
|
|
99
|
+
valueType: ValueType.DOUBLE
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Helper Functions
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
export function recordPaymentAttempt(processor: string, currency: string, status: 'success' | 'failed' | 'pending') {
|
|
107
|
+
paymentCounter.add(1, { processor, currency, status });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function recordPaymentAmount(amount: number, processor: string, currency: string) {
|
|
111
|
+
paymentAmountHistogram.record(amount, { processor, currency });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function recordPaymentLatency(durationMs: number, processor: string, status: 'success' | 'failed') {
|
|
115
|
+
paymentLatencyHistogram.record(durationMs, { processor, status });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function recordWebhookDelivery(status: 'success' | 'failed', attempt: number) {
|
|
119
|
+
webhookDeliveryCounter.add(1, { status, attempt: String(attempt) });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function recordWebhookLatency(durationMs: number, status: 'success' | 'failed') {
|
|
123
|
+
webhookLatencyHistogram.record(durationMs, { status });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function recordHttpRequest(method: string, path: string, statusCode: number, durationMs: number) {
|
|
127
|
+
const statusClass = `${Math.floor(statusCode / 100)}xx`;
|
|
128
|
+
httpRequestCounter.add(1, { method, path, status_code: String(statusCode), status_class: statusClass });
|
|
129
|
+
httpRequestLatencyHistogram.record(durationMs, { method, path, status_class: statusClass });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function recordWorkflowStarted(workflowType: string, taskQueue: string) {
|
|
133
|
+
workflowStartedCounter.add(1, { workflow_type: workflowType, task_queue: taskQueue });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function recordWorkflowCompleted(workflowType: string, taskQueue: string, durationMs: number) {
|
|
137
|
+
workflowCompletedCounter.add(1, { workflow_type: workflowType, task_queue: taskQueue });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function recordWorkflowFailed(workflowType: string, taskQueue: string, errorType: string) {
|
|
141
|
+
workflowFailedCounter.add(1, { workflow_type: workflowType, task_queue: taskQueue, error_type: errorType });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function recordActivityLatency(activityType: string, durationMs: number, status: 'success' | 'failed') {
|
|
145
|
+
activityLatencyHistogram.record(durationMs, { activity_type: activityType, status });
|
|
146
|
+
}
|
package/src/lib/otel.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
2
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
3
|
+
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
|
4
|
+
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
|
|
5
|
+
import { Resource } from '@opentelemetry/resources';
|
|
6
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
|
7
|
+
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
|
8
|
+
|
|
9
|
+
let sdk: NodeSDK | null = null;
|
|
10
|
+
|
|
11
|
+
export interface TelemetryConfig {
|
|
12
|
+
serviceName: string;
|
|
13
|
+
serviceVersion?: string;
|
|
14
|
+
otlpEndpoint?: string;
|
|
15
|
+
environment?: string;
|
|
16
|
+
enabledInstrumentations?: {
|
|
17
|
+
fs?: boolean;
|
|
18
|
+
http?: boolean;
|
|
19
|
+
pg?: boolean;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function initTelemetry(config: TelemetryConfig | string, serviceVersion = '0.0.1'): NodeSDK {
|
|
24
|
+
if (sdk) return sdk;
|
|
25
|
+
|
|
26
|
+
// Support both string (legacy) and config object
|
|
27
|
+
const cfg: TelemetryConfig =
|
|
28
|
+
typeof config === 'string' ? { serviceName: config, serviceVersion } : config;
|
|
29
|
+
|
|
30
|
+
const otlpEndpoint = cfg.otlpEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318';
|
|
31
|
+
const environment = cfg.environment || process.env.NODE_ENV || 'development';
|
|
32
|
+
|
|
33
|
+
sdk = new NodeSDK({
|
|
34
|
+
resource: new Resource({
|
|
35
|
+
[ATTR_SERVICE_NAME]: cfg.serviceName,
|
|
36
|
+
[ATTR_SERVICE_VERSION]: cfg.serviceVersion || '0.0.1',
|
|
37
|
+
'deployment.environment': environment
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
traceExporter: new OTLPTraceExporter({
|
|
41
|
+
url: `${otlpEndpoint}/v1/traces`
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
metricReader: new PeriodicExportingMetricReader({
|
|
45
|
+
exporter: new OTLPMetricExporter({
|
|
46
|
+
url: `${otlpEndpoint}/v1/metrics`
|
|
47
|
+
}),
|
|
48
|
+
exportIntervalMillis: 30000
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
instrumentations: [
|
|
52
|
+
getNodeAutoInstrumentations({
|
|
53
|
+
'@opentelemetry/instrumentation-fs': { enabled: cfg.enabledInstrumentations?.fs ?? false },
|
|
54
|
+
'@opentelemetry/instrumentation-http': { enabled: cfg.enabledInstrumentations?.http ?? true },
|
|
55
|
+
'@opentelemetry/instrumentation-pg': { enabled: cfg.enabledInstrumentations?.pg ?? true }
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
sdk.start();
|
|
61
|
+
|
|
62
|
+
process.on('SIGTERM', () => {
|
|
63
|
+
sdk
|
|
64
|
+
?.shutdown()
|
|
65
|
+
.then(() => console.log('Telemetry shut down'))
|
|
66
|
+
.catch((err) => console.error('Telemetry shutdown error', err));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return sdk;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function shutdownTelemetry(): Promise<void> {
|
|
73
|
+
if (!sdk) return Promise.resolve();
|
|
74
|
+
return sdk.shutdown();
|
|
75
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"outDir": "dist",
|
|
12
|
+
"rootDir": "src"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|