@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 ADDED
@@ -0,0 +1,4 @@
1
+ # OpenTelemetry
2
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
3
+ OTEL_SERVICE_NAME=loop-service
4
+ NODE_ENV=development
@@ -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` |
@@ -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
+ }
@@ -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
+ }