@bsb/observable-opentelemetry 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +665 -0
- package/README.md +58 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.js +43 -0
- package/lib/plugins/observable-opentelemetry/index.d.ts +134 -0
- package/lib/plugins/observable-opentelemetry/index.js +401 -0
- package/lib/schemas/observable-opentelemetry.json +124 -0
- package/lib/schemas/observable-opentelemetry.plugin.json +117 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @bsb/observable-opentelemetry
|
|
2
|
+
|
|
3
|
+
OpenTelemetry observable plugin for BSB with full OTLP export support for traces, metrics, and logs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @bsb/observable-opentelemetry
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Full OpenTelemetry integration** - Traces, metrics, and logs
|
|
14
|
+
- **OTLP export** - HTTP and gRPC protocol support
|
|
15
|
+
- **Resource attributes** - Customizable service metadata
|
|
16
|
+
- **Configurable sampling** - Control trace collection rates
|
|
17
|
+
- **Batch processing** - Efficient data export with batching
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
plugins:
|
|
23
|
+
observables:
|
|
24
|
+
- plugin: "@bsb/observable-opentelemetry"
|
|
25
|
+
enabled: true
|
|
26
|
+
config:
|
|
27
|
+
serviceName: "my-service"
|
|
28
|
+
serviceVersion: "1.0.0"
|
|
29
|
+
endpoint: "http://localhost:4318"
|
|
30
|
+
export:
|
|
31
|
+
protocol: "http"
|
|
32
|
+
interval: 5000
|
|
33
|
+
maxBatchSize: 512
|
|
34
|
+
enabled:
|
|
35
|
+
traces: true
|
|
36
|
+
metrics: true
|
|
37
|
+
logs: true
|
|
38
|
+
resourceAttributes:
|
|
39
|
+
environment: "production"
|
|
40
|
+
region: "us-east-1"
|
|
41
|
+
samplingRate: 1.0
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Configuration Options
|
|
45
|
+
|
|
46
|
+
- **serviceName**: Name of your service
|
|
47
|
+
- **serviceVersion**: Version of your service
|
|
48
|
+
- **endpoint**: OTLP collector endpoint URL
|
|
49
|
+
- **export.protocol**: "http" or "grpc"
|
|
50
|
+
- **export.interval**: Export interval in milliseconds
|
|
51
|
+
- **export.maxBatchSize**: Maximum batch size for exports
|
|
52
|
+
- **enabled**: Toggle traces, metrics, and logs individually
|
|
53
|
+
- **resourceAttributes**: Custom resource attributes
|
|
54
|
+
- **samplingRate**: Trace sampling rate (0.0 to 1.0)
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
Dual-licensed under AGPL-3.0-only OR Commercial License.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BSB (Better-Service-Base) is an event-bus based microservice framework.
|
|
3
|
+
* Copyright (C) 2016 - 2025 BetterCorp (PTY) Ltd
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
7
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* Alternatively, you may obtain a commercial license for this program.
|
|
11
|
+
* The commercial license allows you to use the Program in a closed-source manner,
|
|
12
|
+
* including the right to create derivative works that are not subject to the terms
|
|
13
|
+
* of the AGPL.
|
|
14
|
+
*
|
|
15
|
+
* To obtain a commercial license, please contact the copyright holders at
|
|
16
|
+
* https://www.bettercorp.dev. The terms and conditions of the commercial license
|
|
17
|
+
* will be provided upon request.
|
|
18
|
+
*
|
|
19
|
+
* This program is distributed in the hope that it will be useful,
|
|
20
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
21
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
22
|
+
* GNU Affero General Public License for more details.
|
|
23
|
+
*
|
|
24
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
25
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
26
|
+
*/
|
|
27
|
+
export * from "./plugins/observable-opentelemetry";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BSB (Better-Service-Base) is an event-bus based microservice framework.
|
|
4
|
+
* Copyright (C) 2016 - 2025 BetterCorp (PTY) Ltd
|
|
5
|
+
*
|
|
6
|
+
* This program is free software: you can redistribute it and/or modify
|
|
7
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
8
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
* (at your option) any later version.
|
|
10
|
+
*
|
|
11
|
+
* Alternatively, you may obtain a commercial license for this program.
|
|
12
|
+
* The commercial license allows you to use the Program in a closed-source manner,
|
|
13
|
+
* including the right to create derivative works that are not subject to the terms
|
|
14
|
+
* of the AGPL.
|
|
15
|
+
*
|
|
16
|
+
* To obtain a commercial license, please contact the copyright holders at
|
|
17
|
+
* https://www.bettercorp.dev. The terms and conditions of the commercial license
|
|
18
|
+
* will be provided upon request.
|
|
19
|
+
*
|
|
20
|
+
* This program is distributed in the hope that it will be useful,
|
|
21
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
22
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
23
|
+
* GNU Affero General Public License for more details.
|
|
24
|
+
*
|
|
25
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
26
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
27
|
+
*/
|
|
28
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
31
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
32
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
33
|
+
}
|
|
34
|
+
Object.defineProperty(o, k2, desc);
|
|
35
|
+
}) : (function(o, m, k, k2) {
|
|
36
|
+
if (k2 === undefined) k2 = k;
|
|
37
|
+
o[k2] = m[k];
|
|
38
|
+
}));
|
|
39
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
40
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
__exportStar(require("./plugins/observable-opentelemetry"), exports);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BSB (Better-Service-Base) is an event-bus based microservice framework.
|
|
3
|
+
* Copyright (C) 2016 - 2025 BetterCorp (PTY) Ltd
|
|
4
|
+
*
|
|
5
|
+
* This program is free software: you can redistribute it and/or modify
|
|
6
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
7
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
* (at your option) any later version.
|
|
9
|
+
*
|
|
10
|
+
* Alternatively, you may obtain a commercial license for this program.
|
|
11
|
+
* The commercial license allows you to use the Program in a closed-source manner,
|
|
12
|
+
* including the right to create derivative works that are not subject to the terms
|
|
13
|
+
* of the AGPL.
|
|
14
|
+
*
|
|
15
|
+
* To obtain a commercial license, please contact the copyright holders at
|
|
16
|
+
* https://www.bettercorp.dev. The terms and conditions of the commercial license
|
|
17
|
+
* will be provided upon request.
|
|
18
|
+
*
|
|
19
|
+
* This program is distributed in the hope that it will be useful,
|
|
20
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
21
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
22
|
+
* GNU Affero General Public License for more details.
|
|
23
|
+
*
|
|
24
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
25
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
26
|
+
*/
|
|
27
|
+
import { BSBObservable, BSBObservableConstructor, BSBError } from "@bsb/base";
|
|
28
|
+
import { DTrace, LogMeta } from "@bsb/base";
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
/**
|
|
31
|
+
* Configuration schema for OpenTelemetry plugin
|
|
32
|
+
*/
|
|
33
|
+
export declare const OpenTelemetryConfigSchema: z.ZodObject<{
|
|
34
|
+
serviceName: z.ZodDefault<z.ZodString>;
|
|
35
|
+
serviceVersion: z.ZodOptional<z.ZodString>;
|
|
36
|
+
endpoint: z.ZodDefault<z.ZodString>;
|
|
37
|
+
export: z.ZodObject<{
|
|
38
|
+
protocol: z.ZodDefault<z.ZodEnum<{
|
|
39
|
+
http: "http";
|
|
40
|
+
grpc: "grpc";
|
|
41
|
+
}>>;
|
|
42
|
+
interval: z.ZodDefault<z.ZodNumber>;
|
|
43
|
+
maxBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
enabled: z.ZodObject<{
|
|
46
|
+
traces: z.ZodDefault<z.ZodBoolean>;
|
|
47
|
+
metrics: z.ZodDefault<z.ZodBoolean>;
|
|
48
|
+
logs: z.ZodDefault<z.ZodBoolean>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
resourceAttributes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
51
|
+
samplingRate: z.ZodDefault<z.ZodNumber>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
export type OpenTelemetryConfig = z.infer<typeof OpenTelemetryConfigSchema>;
|
|
54
|
+
export declare const Config: import("@bsb/base").BSBPluginConfigClass<z.ZodObject<{
|
|
55
|
+
serviceName: z.ZodDefault<z.ZodString>;
|
|
56
|
+
serviceVersion: z.ZodOptional<z.ZodString>;
|
|
57
|
+
endpoint: z.ZodDefault<z.ZodString>;
|
|
58
|
+
export: z.ZodObject<{
|
|
59
|
+
protocol: z.ZodDefault<z.ZodEnum<{
|
|
60
|
+
http: "http";
|
|
61
|
+
grpc: "grpc";
|
|
62
|
+
}>>;
|
|
63
|
+
interval: z.ZodDefault<z.ZodNumber>;
|
|
64
|
+
maxBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
enabled: z.ZodObject<{
|
|
67
|
+
traces: z.ZodDefault<z.ZodBoolean>;
|
|
68
|
+
metrics: z.ZodDefault<z.ZodBoolean>;
|
|
69
|
+
logs: z.ZodDefault<z.ZodBoolean>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
resourceAttributes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
72
|
+
samplingRate: z.ZodDefault<z.ZodNumber>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
/**
|
|
75
|
+
* OpenTelemetry observable plugin with OTLP export
|
|
76
|
+
*/
|
|
77
|
+
export declare class Plugin extends BSBObservable<InstanceType<typeof Config>> {
|
|
78
|
+
static Config: import("@bsb/base").BSBPluginConfigClass<z.ZodObject<{
|
|
79
|
+
serviceName: z.ZodDefault<z.ZodString>;
|
|
80
|
+
serviceVersion: z.ZodOptional<z.ZodString>;
|
|
81
|
+
endpoint: z.ZodDefault<z.ZodString>;
|
|
82
|
+
export: z.ZodObject<{
|
|
83
|
+
protocol: z.ZodDefault<z.ZodEnum<{
|
|
84
|
+
http: "http";
|
|
85
|
+
grpc: "grpc";
|
|
86
|
+
}>>;
|
|
87
|
+
interval: z.ZodDefault<z.ZodNumber>;
|
|
88
|
+
maxBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
enabled: z.ZodObject<{
|
|
91
|
+
traces: z.ZodDefault<z.ZodBoolean>;
|
|
92
|
+
metrics: z.ZodDefault<z.ZodBoolean>;
|
|
93
|
+
logs: z.ZodDefault<z.ZodBoolean>;
|
|
94
|
+
}, z.core.$strip>;
|
|
95
|
+
resourceAttributes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
96
|
+
samplingRate: z.ZodDefault<z.ZodNumber>;
|
|
97
|
+
}, z.core.$strip>>;
|
|
98
|
+
private logFormatter;
|
|
99
|
+
private sdk;
|
|
100
|
+
private tracer;
|
|
101
|
+
private meter;
|
|
102
|
+
private logger;
|
|
103
|
+
private loggerProvider;
|
|
104
|
+
private isDisposed;
|
|
105
|
+
private counters;
|
|
106
|
+
private gauges;
|
|
107
|
+
private histograms;
|
|
108
|
+
private spans;
|
|
109
|
+
constructor(config: BSBObservableConstructor<InstanceType<typeof Config>>);
|
|
110
|
+
init(): Promise<void>;
|
|
111
|
+
run(): Promise<void>;
|
|
112
|
+
/**
|
|
113
|
+
* Map DTrace to OpenTelemetry trace context
|
|
114
|
+
*/
|
|
115
|
+
private getTraceContext;
|
|
116
|
+
/**
|
|
117
|
+
* Write log entry via OpenTelemetry
|
|
118
|
+
*/
|
|
119
|
+
private writeLog;
|
|
120
|
+
debug(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
|
|
121
|
+
info(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
|
|
122
|
+
warn(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
|
|
123
|
+
error(trace: DTrace, pluginName: string, message: string | BSBError<any>, meta?: LogMeta<any>): void;
|
|
124
|
+
createCounter(timestamp: number, pluginName: string, name: string, description: string, help: string, labels?: string[]): void;
|
|
125
|
+
incrementCounter(timestamp: number, pluginName: string, name: string, value: number, labels?: Record<string, string>): void;
|
|
126
|
+
createGauge(timestamp: number, pluginName: string, name: string, description: string, help: string, labels?: string[]): void;
|
|
127
|
+
setGauge(timestamp: number, pluginName: string, name: string, value: number, labels?: Record<string, string>): void;
|
|
128
|
+
createHistogram(timestamp: number, pluginName: string, name: string, description: string, help: string, boundaries?: number[], labels?: string[]): void;
|
|
129
|
+
observeHistogram(timestamp: number, pluginName: string, name: string, value: number, labels?: Record<string, string>): void;
|
|
130
|
+
spanStart(trace: DTrace, pluginName: string, spanName: string, parentSpanId: string | null, attributes?: Record<string, string | number | boolean>): void;
|
|
131
|
+
spanEnd(trace: DTrace, pluginName: string, attributes?: Record<string, string | number | boolean>): void;
|
|
132
|
+
spanError(trace: DTrace, pluginName: string, error: Error, attributes?: Record<string, string | number | boolean>): void;
|
|
133
|
+
dispose(): Promise<void>;
|
|
134
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BSB (Better-Service-Base) is an event-bus based microservice framework.
|
|
4
|
+
* Copyright (C) 2016 - 2025 BetterCorp (PTY) Ltd
|
|
5
|
+
*
|
|
6
|
+
* This program is free software: you can redistribute it and/or modify
|
|
7
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
8
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
9
|
+
* (at your option) any later version.
|
|
10
|
+
*
|
|
11
|
+
* Alternatively, you may obtain a commercial license for this program.
|
|
12
|
+
* The commercial license allows you to use the Program in a closed-source manner,
|
|
13
|
+
* including the right to create derivative works that are not subject to the terms
|
|
14
|
+
* of the AGPL.
|
|
15
|
+
*
|
|
16
|
+
* To obtain a commercial license, please contact the copyright holders at
|
|
17
|
+
* https://www.bettercorp.dev. The terms and conditions of the commercial license
|
|
18
|
+
* will be provided upon request.
|
|
19
|
+
*
|
|
20
|
+
* This program is distributed in the hope that it will be useful,
|
|
21
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
22
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
23
|
+
* GNU Affero General Public License for more details.
|
|
24
|
+
*
|
|
25
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
26
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
27
|
+
*/
|
|
28
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
31
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
32
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
33
|
+
}
|
|
34
|
+
Object.defineProperty(o, k2, desc);
|
|
35
|
+
}) : (function(o, m, k, k2) {
|
|
36
|
+
if (k2 === undefined) k2 = k;
|
|
37
|
+
o[k2] = m[k];
|
|
38
|
+
}));
|
|
39
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
40
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
41
|
+
}) : function(o, v) {
|
|
42
|
+
o["default"] = v;
|
|
43
|
+
});
|
|
44
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
45
|
+
var ownKeys = function(o) {
|
|
46
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
47
|
+
var ar = [];
|
|
48
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
49
|
+
return ar;
|
|
50
|
+
};
|
|
51
|
+
return ownKeys(o);
|
|
52
|
+
};
|
|
53
|
+
return function (mod) {
|
|
54
|
+
if (mod && mod.__esModule) return mod;
|
|
55
|
+
var result = {};
|
|
56
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
57
|
+
__setModuleDefault(result, mod);
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
61
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
+
exports.Plugin = exports.Config = exports.OpenTelemetryConfigSchema = void 0;
|
|
63
|
+
const base_1 = require("@bsb/base");
|
|
64
|
+
const zod_1 = require("zod");
|
|
65
|
+
const api = __importStar(require("@opentelemetry/api"));
|
|
66
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
67
|
+
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
|
|
68
|
+
const sdk_node_1 = require("@opentelemetry/sdk-node");
|
|
69
|
+
const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
|
|
70
|
+
const exporter_metrics_otlp_http_1 = require("@opentelemetry/exporter-metrics-otlp-http");
|
|
71
|
+
const exporter_logs_otlp_http_1 = require("@opentelemetry/exporter-logs-otlp-http");
|
|
72
|
+
const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
|
|
73
|
+
const sdk_logs_1 = require("@opentelemetry/sdk-logs");
|
|
74
|
+
/**
|
|
75
|
+
* Configuration schema for OpenTelemetry plugin
|
|
76
|
+
*/
|
|
77
|
+
exports.OpenTelemetryConfigSchema = zod_1.z.object({
|
|
78
|
+
serviceName: zod_1.z.string().default("bsb-service"),
|
|
79
|
+
serviceVersion: zod_1.z.string().optional(),
|
|
80
|
+
endpoint: zod_1.z.string().url().default("http://localhost:4318"),
|
|
81
|
+
export: zod_1.z.object({
|
|
82
|
+
protocol: zod_1.z.enum(["http", "grpc"]).default("http"),
|
|
83
|
+
interval: zod_1.z.number().int().min(100).default(5000),
|
|
84
|
+
maxBatchSize: zod_1.z.number().int().min(1).default(512),
|
|
85
|
+
}),
|
|
86
|
+
enabled: zod_1.z.object({
|
|
87
|
+
traces: zod_1.z.boolean().default(true),
|
|
88
|
+
metrics: zod_1.z.boolean().default(true),
|
|
89
|
+
logs: zod_1.z.boolean().default(true),
|
|
90
|
+
}),
|
|
91
|
+
resourceAttributes: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).default({}),
|
|
92
|
+
samplingRate: zod_1.z.number().min(0).max(1).default(1.0),
|
|
93
|
+
});
|
|
94
|
+
exports.Config = (0, base_1.createConfigSchema)({
|
|
95
|
+
name: 'observable-opentelemetry',
|
|
96
|
+
description: 'OpenTelemetry integration for logs, metrics, and traces via OTLP',
|
|
97
|
+
version: '9.0.0',
|
|
98
|
+
image: './observable-opentelemetry.png',
|
|
99
|
+
tags: ['opentelemetry', 'otlp', 'observability', 'logs', 'metrics', 'traces'],
|
|
100
|
+
documentation: ['./docs/plugin.md'],
|
|
101
|
+
}, exports.OpenTelemetryConfigSchema);
|
|
102
|
+
/**
|
|
103
|
+
* Convert BSB log level to OpenTelemetry severity number
|
|
104
|
+
*/
|
|
105
|
+
function bsbLevelToOtelSeverity(level) {
|
|
106
|
+
const normalized = level.toLowerCase();
|
|
107
|
+
switch (normalized) {
|
|
108
|
+
case "debug":
|
|
109
|
+
return 5; // DEBUG
|
|
110
|
+
case "info":
|
|
111
|
+
return 9; // INFO
|
|
112
|
+
case "warn":
|
|
113
|
+
case "warning":
|
|
114
|
+
return 13; // WARN
|
|
115
|
+
case "error":
|
|
116
|
+
return 17; // ERROR
|
|
117
|
+
default:
|
|
118
|
+
return 9; // Default to INFO
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Convert BSB log level to OpenTelemetry severity text
|
|
123
|
+
*/
|
|
124
|
+
function bsbLevelToOtelSeverityText(level) {
|
|
125
|
+
const normalized = level.toLowerCase();
|
|
126
|
+
switch (normalized) {
|
|
127
|
+
case "debug":
|
|
128
|
+
return "DEBUG";
|
|
129
|
+
case "info":
|
|
130
|
+
return "INFO";
|
|
131
|
+
case "warn":
|
|
132
|
+
case "warning":
|
|
133
|
+
return "WARN";
|
|
134
|
+
case "error":
|
|
135
|
+
return "ERROR";
|
|
136
|
+
default:
|
|
137
|
+
return "INFO";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* OpenTelemetry observable plugin with OTLP export
|
|
142
|
+
*/
|
|
143
|
+
class Plugin extends base_1.BSBObservable {
|
|
144
|
+
static Config = exports.Config;
|
|
145
|
+
logFormatter = new base_1.LogFormatter();
|
|
146
|
+
sdk = null;
|
|
147
|
+
tracer = null;
|
|
148
|
+
meter = null;
|
|
149
|
+
logger = null;
|
|
150
|
+
loggerProvider = null;
|
|
151
|
+
isDisposed = false;
|
|
152
|
+
// Cache for metrics
|
|
153
|
+
counters = new Map();
|
|
154
|
+
gauges = new Map();
|
|
155
|
+
histograms = new Map();
|
|
156
|
+
// Cache for active spans
|
|
157
|
+
spans = new Map();
|
|
158
|
+
constructor(config) {
|
|
159
|
+
super(config);
|
|
160
|
+
}
|
|
161
|
+
async init() {
|
|
162
|
+
// Create resource with service information
|
|
163
|
+
const resource = new resources_1.Resource({
|
|
164
|
+
[semantic_conventions_1.ATTR_SERVICE_NAME]: this.config.serviceName,
|
|
165
|
+
...(this.config.serviceVersion && { [semantic_conventions_1.ATTR_SERVICE_VERSION]: this.config.serviceVersion }),
|
|
166
|
+
...this.config.resourceAttributes,
|
|
167
|
+
});
|
|
168
|
+
// Configure exporters
|
|
169
|
+
const traceExporter = new exporter_trace_otlp_http_1.OTLPTraceExporter({
|
|
170
|
+
url: `${this.config.endpoint}/v1/traces`,
|
|
171
|
+
});
|
|
172
|
+
const metricExporter = new exporter_metrics_otlp_http_1.OTLPMetricExporter({
|
|
173
|
+
url: `${this.config.endpoint}/v1/metrics`,
|
|
174
|
+
});
|
|
175
|
+
const logExporter = new exporter_logs_otlp_http_1.OTLPLogExporter({
|
|
176
|
+
url: `${this.config.endpoint}/v1/logs`,
|
|
177
|
+
});
|
|
178
|
+
// Initialize SDK
|
|
179
|
+
this.sdk = new sdk_node_1.NodeSDK({
|
|
180
|
+
resource,
|
|
181
|
+
traceExporter: this.config.enabled.traces ? traceExporter : undefined,
|
|
182
|
+
});
|
|
183
|
+
await this.sdk.start();
|
|
184
|
+
// Get tracer
|
|
185
|
+
if (this.config.enabled.traces) {
|
|
186
|
+
this.tracer = api.trace.getTracer(this.config.serviceName, this.config.serviceVersion);
|
|
187
|
+
}
|
|
188
|
+
// Setup metrics separately
|
|
189
|
+
if (this.config.enabled.metrics) {
|
|
190
|
+
const meterProvider = new sdk_metrics_1.MeterProvider({ resource });
|
|
191
|
+
meterProvider.addMetricReader(new sdk_metrics_1.PeriodicExportingMetricReader({
|
|
192
|
+
exporter: metricExporter,
|
|
193
|
+
exportIntervalMillis: this.config.export.interval,
|
|
194
|
+
}));
|
|
195
|
+
this.meter = meterProvider.getMeter(this.config.serviceName, this.config.serviceVersion);
|
|
196
|
+
}
|
|
197
|
+
// Setup logs separately
|
|
198
|
+
if (this.config.enabled.logs) {
|
|
199
|
+
this.loggerProvider = new sdk_logs_1.LoggerProvider({ resource });
|
|
200
|
+
this.loggerProvider.addLogRecordProcessor(new sdk_logs_1.BatchLogRecordProcessor(logExporter));
|
|
201
|
+
this.logger = this.loggerProvider.getLogger(this.config.serviceName, this.config.serviceVersion);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async run() {
|
|
205
|
+
// No runtime setup needed
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Map DTrace to OpenTelemetry trace context
|
|
209
|
+
*/
|
|
210
|
+
getTraceContext(trace) {
|
|
211
|
+
// Convert DTrace.t (trace ID) and DTrace.s (span ID) to OpenTelemetry format
|
|
212
|
+
// BSB uses string IDs, OpenTelemetry uses hex format
|
|
213
|
+
const traceId = trace.t.padStart(32, "0").substring(0, 32);
|
|
214
|
+
const spanId = trace.s.padStart(16, "0").substring(0, 16);
|
|
215
|
+
const spanContext = {
|
|
216
|
+
traceId,
|
|
217
|
+
spanId,
|
|
218
|
+
traceFlags: api.TraceFlags.SAMPLED,
|
|
219
|
+
};
|
|
220
|
+
return api.trace.setSpanContext(api.context.active(), spanContext);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Write log entry via OpenTelemetry
|
|
224
|
+
*/
|
|
225
|
+
writeLog(level, trace, pluginName, message, meta) {
|
|
226
|
+
if (!this.logger || this.isDisposed) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const formattedMessage = this.logFormatter.formatLog(trace, message, meta);
|
|
230
|
+
const context = this.getTraceContext(trace);
|
|
231
|
+
this.logger.emit({
|
|
232
|
+
severityNumber: bsbLevelToOtelSeverity(level),
|
|
233
|
+
severityText: bsbLevelToOtelSeverityText(level),
|
|
234
|
+
body: formattedMessage,
|
|
235
|
+
attributes: {
|
|
236
|
+
"bsb.plugin": pluginName,
|
|
237
|
+
"bsb.trace.t": trace.t,
|
|
238
|
+
"bsb.trace.s": trace.s,
|
|
239
|
+
...(meta && Object.keys(meta).length > 0 ? { "bsb.meta": JSON.stringify(meta) } : {}),
|
|
240
|
+
},
|
|
241
|
+
context,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// Logging methods
|
|
245
|
+
debug(trace, pluginName, message, meta) {
|
|
246
|
+
if (this.mode === "production") {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.writeLog("debug", trace, pluginName, message, meta);
|
|
250
|
+
}
|
|
251
|
+
info(trace, pluginName, message, meta) {
|
|
252
|
+
this.writeLog("info", trace, pluginName, message, meta);
|
|
253
|
+
}
|
|
254
|
+
warn(trace, pluginName, message, meta) {
|
|
255
|
+
this.writeLog("warn", trace, pluginName, message, meta);
|
|
256
|
+
}
|
|
257
|
+
error(trace, pluginName, message, meta) {
|
|
258
|
+
if (message instanceof base_1.BSBError) {
|
|
259
|
+
if (message.raw !== null) {
|
|
260
|
+
this.writeLog("error", message.raw.trace, pluginName, message.raw.message, message.raw.meta);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
this.writeLog("error", trace, pluginName, message.message);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.writeLog("error", trace, pluginName, message, meta);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Metrics methods
|
|
271
|
+
createCounter(timestamp, pluginName, name, description, help, labels) {
|
|
272
|
+
if (!this.meter || this.isDisposed) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const fullName = `${pluginName}.${name}`;
|
|
276
|
+
if (!this.counters.has(fullName)) {
|
|
277
|
+
const counter = this.meter.createCounter(fullName, {
|
|
278
|
+
description: description || help,
|
|
279
|
+
});
|
|
280
|
+
this.counters.set(fullName, counter);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
incrementCounter(timestamp, pluginName, name, value, labels) {
|
|
284
|
+
if (!this.meter || this.isDisposed) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const fullName = `${pluginName}.${name}`;
|
|
288
|
+
const counter = this.counters.get(fullName);
|
|
289
|
+
if (counter) {
|
|
290
|
+
counter.add(value, labels);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
createGauge(timestamp, pluginName, name, description, help, labels) {
|
|
294
|
+
if (!this.meter || this.isDisposed) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const fullName = `${pluginName}.${name}`;
|
|
298
|
+
if (!this.gauges.has(fullName)) {
|
|
299
|
+
let currentValue = 0;
|
|
300
|
+
const gauge = this.meter.createObservableGauge(fullName, {
|
|
301
|
+
description: description || help,
|
|
302
|
+
});
|
|
303
|
+
gauge.addCallback((observableResult) => {
|
|
304
|
+
observableResult.observe(currentValue);
|
|
305
|
+
});
|
|
306
|
+
this.gauges.set(fullName, gauge);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
setGauge(timestamp, pluginName, name, value, labels) {
|
|
310
|
+
// OpenTelemetry ObservableGauge doesn't support direct setting
|
|
311
|
+
// Would need to use UpDownCounter instead or maintain state
|
|
312
|
+
// For now, log a warning
|
|
313
|
+
console.warn(`[observable-opentelemetry] setGauge not fully supported in OpenTelemetry, use UpDownCounter`);
|
|
314
|
+
}
|
|
315
|
+
createHistogram(timestamp, pluginName, name, description, help, boundaries, labels) {
|
|
316
|
+
if (!this.meter || this.isDisposed) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const fullName = `${pluginName}.${name}`;
|
|
320
|
+
if (!this.histograms.has(fullName)) {
|
|
321
|
+
const histogram = this.meter.createHistogram(fullName, {
|
|
322
|
+
description: description || help,
|
|
323
|
+
});
|
|
324
|
+
this.histograms.set(fullName, histogram);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
observeHistogram(timestamp, pluginName, name, value, labels) {
|
|
328
|
+
if (!this.meter || this.isDisposed) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const fullName = `${pluginName}.${name}`;
|
|
332
|
+
const histogram = this.histograms.get(fullName);
|
|
333
|
+
if (histogram) {
|
|
334
|
+
histogram.record(value, labels);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Tracing methods
|
|
338
|
+
spanStart(trace, pluginName, spanName, parentSpanId, attributes) {
|
|
339
|
+
if (!this.tracer || this.isDisposed) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const context = this.getTraceContext(trace);
|
|
343
|
+
const span = this.tracer.startSpan(spanName, {
|
|
344
|
+
attributes: {
|
|
345
|
+
"bsb.plugin": pluginName,
|
|
346
|
+
...(parentSpanId ? { "bsb.parent_span_id": parentSpanId } : {}),
|
|
347
|
+
...attributes,
|
|
348
|
+
},
|
|
349
|
+
}, context);
|
|
350
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
351
|
+
this.spans.set(spanKey, span);
|
|
352
|
+
}
|
|
353
|
+
spanEnd(trace, pluginName, attributes) {
|
|
354
|
+
if (this.isDisposed) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
358
|
+
const span = this.spans.get(spanKey);
|
|
359
|
+
if (span) {
|
|
360
|
+
if (attributes) {
|
|
361
|
+
span.setAttributes(attributes);
|
|
362
|
+
}
|
|
363
|
+
span.end();
|
|
364
|
+
this.spans.delete(spanKey);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
spanError(trace, pluginName, error, attributes) {
|
|
368
|
+
if (this.isDisposed) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
372
|
+
const span = this.spans.get(spanKey);
|
|
373
|
+
if (span) {
|
|
374
|
+
span.recordException(error);
|
|
375
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
|
|
376
|
+
if (attributes) {
|
|
377
|
+
span.setAttributes(attributes);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async dispose() {
|
|
382
|
+
this.isDisposed = true;
|
|
383
|
+
// End all active spans
|
|
384
|
+
for (const span of this.spans.values()) {
|
|
385
|
+
span.end();
|
|
386
|
+
}
|
|
387
|
+
this.spans.clear();
|
|
388
|
+
// Shutdown SDK
|
|
389
|
+
if (this.sdk) {
|
|
390
|
+
await this.sdk.shutdown();
|
|
391
|
+
this.sdk = null;
|
|
392
|
+
}
|
|
393
|
+
this.tracer = null;
|
|
394
|
+
this.meter = null;
|
|
395
|
+
this.logger = null;
|
|
396
|
+
this.counters.clear();
|
|
397
|
+
this.gauges.clear();
|
|
398
|
+
this.histograms.clear();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
exports.Plugin = Plugin;
|