@bsb/observable-zipkin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -0
- package/bsb-plugin.json +18 -0
- package/lib/plugins/observable-zipkin/index.d.ts +120 -0
- package/lib/plugins/observable-zipkin/index.js +296 -0
- package/lib/schemas/observable-zipkin.json +151 -0
- package/lib/schemas/observable-zipkin.plugin.json +139 -0
- package/package.json +49 -0
- package/src/plugins/observable-zipkin/index.ts +348 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# BSB Observable Zipkin
|
|
2
|
+
|
|
3
|
+
Zipkin tracing integration for the Better Service Base (BSB) framework.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Direct Zipkin v2 API integration
|
|
8
|
+
- Distributed tracing with OpenTelemetry instrumentation
|
|
9
|
+
- Configurable sampling rates
|
|
10
|
+
- Batch span export with configurable flush intervals
|
|
11
|
+
- Console logging fallback for non-trace observability
|
|
12
|
+
- Full TypeScript support
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @bsb/observable-zipkin
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
Add to your BSB configuration file:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
plugins:
|
|
26
|
+
observable:
|
|
27
|
+
- observable-zipkin
|
|
28
|
+
|
|
29
|
+
observable-zipkin:
|
|
30
|
+
serviceName: my-service
|
|
31
|
+
serviceVersion: 1.0.0
|
|
32
|
+
|
|
33
|
+
zipkin:
|
|
34
|
+
url: http://localhost:9411/api/v2/spans
|
|
35
|
+
# Optional custom headers for authentication
|
|
36
|
+
# headers:
|
|
37
|
+
# Authorization: Bearer token123
|
|
38
|
+
|
|
39
|
+
export:
|
|
40
|
+
maxBatchSize: 100
|
|
41
|
+
maxQueueSize: 2048
|
|
42
|
+
scheduledDelayMillis: 5000
|
|
43
|
+
|
|
44
|
+
samplingRate: 1.0 # 1.0 = 100%, 0.5 = 50%
|
|
45
|
+
|
|
46
|
+
# Console logging (Zipkin doesn't handle logs/metrics)
|
|
47
|
+
console:
|
|
48
|
+
enabled: true
|
|
49
|
+
logLevel: info # debug | info | warn | error
|
|
50
|
+
|
|
51
|
+
# Optional resource attributes
|
|
52
|
+
resourceAttributes:
|
|
53
|
+
environment: production
|
|
54
|
+
region: us-east-1
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What Gets Exported
|
|
58
|
+
|
|
59
|
+
### Traces (to Zipkin)
|
|
60
|
+
- All span lifecycle events (start, end, error)
|
|
61
|
+
- Span attributes and context
|
|
62
|
+
- Distributed trace propagation
|
|
63
|
+
- Exception tracking
|
|
64
|
+
|
|
65
|
+
### Logs (to Console)
|
|
66
|
+
- Debug, info, warn, error levels
|
|
67
|
+
- Structured logging with trace context
|
|
68
|
+
- Configurable log level filtering
|
|
69
|
+
|
|
70
|
+
### Metrics (No-op)
|
|
71
|
+
- Zipkin is trace-only, no metrics support
|
|
72
|
+
- Use `observable-opentelemetry` or `observable-prometheus` for metrics
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
The plugin automatically integrates with BSB's Observable pattern:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
export class MyService extends BSBService<InstanceType<typeof Config>, typeof EventSchemas> {
|
|
80
|
+
static Config = Config;
|
|
81
|
+
static EventSchemas = EventSchemas;
|
|
82
|
+
|
|
83
|
+
public async run(obs: Observable) {
|
|
84
|
+
// Traces automatically sent to Zipkin
|
|
85
|
+
obs.log.info("Service started"); // Console log
|
|
86
|
+
|
|
87
|
+
// Create child span
|
|
88
|
+
const workObs = obs.span("heavy-work");
|
|
89
|
+
workObs.setAttribute("work.type", "batch");
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await this.doWork();
|
|
93
|
+
workObs.end();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
workObs.recordException(error);
|
|
96
|
+
workObs.end();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Zipkin Setup
|
|
103
|
+
|
|
104
|
+
Run Zipkin locally with Docker:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
docker run -d -p 9411:9411 openzipkin/zipkin
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Access UI at: http://localhost:9411
|
|
111
|
+
|
|
112
|
+
## Architecture
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
BSB Application
|
|
116
|
+
↓
|
|
117
|
+
Observable (DTrace)
|
|
118
|
+
↓
|
|
119
|
+
observable-zipkin
|
|
120
|
+
↓
|
|
121
|
+
OpenTelemetry SDK
|
|
122
|
+
↓
|
|
123
|
+
Zipkin Exporter
|
|
124
|
+
↓
|
|
125
|
+
Zipkin Server (HTTP)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Configuration Options
|
|
129
|
+
|
|
130
|
+
| Option | Type | Default | Description |
|
|
131
|
+
|--------|------|---------|-------------|
|
|
132
|
+
| `serviceName` | string | `"bsb-service"` | Service identifier in Zipkin |
|
|
133
|
+
| `serviceVersion` | string | - | Service version tag |
|
|
134
|
+
| `zipkin.url` | string | `"http://localhost:9411/api/v2/spans"` | Zipkin API endpoint |
|
|
135
|
+
| `zipkin.headers` | object | - | Custom HTTP headers |
|
|
136
|
+
| `export.maxBatchSize` | number | `100` | Max spans per batch |
|
|
137
|
+
| `export.maxQueueSize` | number | `2048` | Max queued spans |
|
|
138
|
+
| `export.scheduledDelayMillis` | number | `5000` | Flush interval (ms) |
|
|
139
|
+
| `samplingRate` | number | `1.0` | Sampling probability (0-1) |
|
|
140
|
+
| `console.enabled` | boolean | `true` | Enable console logs |
|
|
141
|
+
| `console.logLevel` | string | `"info"` | Minimum log level |
|
|
142
|
+
|
|
143
|
+
## Comparison with Other Observability Plugins
|
|
144
|
+
|
|
145
|
+
| Plugin | Traces | Metrics | Logs | Backend |
|
|
146
|
+
|--------|--------|---------|------|---------|
|
|
147
|
+
| observable-zipkin | ✅ | ❌ | Console | Zipkin |
|
|
148
|
+
| observable-opentelemetry | ✅ | ✅ | ✅ | OTLP Collector |
|
|
149
|
+
| observable-default | ❌ | ❌ | Console | - |
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
AGPL-3.0 - See LICENSE file for details.
|
|
154
|
+
|
|
155
|
+
Commercial licenses available at https://www.bettercorp.dev
|
package/bsb-plugin.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodejs": [
|
|
3
|
+
{
|
|
4
|
+
"id": "observable-zipkin",
|
|
5
|
+
"name": "observable-zipkin",
|
|
6
|
+
"basePath": "./",
|
|
7
|
+
"description": "Zipkin tracing integration for BSB framework",
|
|
8
|
+
"tags": [
|
|
9
|
+
"zipkin",
|
|
10
|
+
"tracing",
|
|
11
|
+
"observability",
|
|
12
|
+
"distributed-tracing"
|
|
13
|
+
],
|
|
14
|
+
"documentation": [],
|
|
15
|
+
"pluginPath": "src/plugins/observable-zipkin/"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
export declare const Config: import("@bsb/base").BSBPluginConfigClass<z.ZodObject<{
|
|
31
|
+
serviceName: z.ZodDefault<z.ZodString>;
|
|
32
|
+
serviceVersion: z.ZodOptional<z.ZodString>;
|
|
33
|
+
zipkin: z.ZodObject<{
|
|
34
|
+
url: z.ZodDefault<z.ZodString>;
|
|
35
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
36
|
+
statusCodeTagName: z.ZodDefault<z.ZodString>;
|
|
37
|
+
statusDescriptionTagName: z.ZodDefault<z.ZodString>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export: z.ZodObject<{
|
|
40
|
+
maxBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
41
|
+
maxQueueSize: z.ZodDefault<z.ZodNumber>;
|
|
42
|
+
scheduledDelayMillis: z.ZodDefault<z.ZodNumber>;
|
|
43
|
+
}, z.core.$strip>;
|
|
44
|
+
resourceAttributes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
45
|
+
samplingRate: z.ZodDefault<z.ZodNumber>;
|
|
46
|
+
console: z.ZodObject<{
|
|
47
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
48
|
+
logLevel: z.ZodDefault<z.ZodEnum<{
|
|
49
|
+
error: "error";
|
|
50
|
+
debug: "debug";
|
|
51
|
+
info: "info";
|
|
52
|
+
warn: "warn";
|
|
53
|
+
}>>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
}, z.core.$strip>>;
|
|
56
|
+
/**
|
|
57
|
+
* Zipkin observable plugin for distributed tracing
|
|
58
|
+
*
|
|
59
|
+
* Exports traces directly to Zipkin using the Zipkin v2 API format.
|
|
60
|
+
* Provides console logging fallback for non-trace observability.
|
|
61
|
+
*/
|
|
62
|
+
export declare class Plugin extends BSBObservable<InstanceType<typeof Config>> {
|
|
63
|
+
static Config: import("@bsb/base").BSBPluginConfigClass<z.ZodObject<{
|
|
64
|
+
serviceName: z.ZodDefault<z.ZodString>;
|
|
65
|
+
serviceVersion: z.ZodOptional<z.ZodString>;
|
|
66
|
+
zipkin: z.ZodObject<{
|
|
67
|
+
url: z.ZodDefault<z.ZodString>;
|
|
68
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
69
|
+
statusCodeTagName: z.ZodDefault<z.ZodString>;
|
|
70
|
+
statusDescriptionTagName: z.ZodDefault<z.ZodString>;
|
|
71
|
+
}, z.core.$strip>;
|
|
72
|
+
export: z.ZodObject<{
|
|
73
|
+
maxBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
74
|
+
maxQueueSize: z.ZodDefault<z.ZodNumber>;
|
|
75
|
+
scheduledDelayMillis: z.ZodDefault<z.ZodNumber>;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
resourceAttributes: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
78
|
+
samplingRate: z.ZodDefault<z.ZodNumber>;
|
|
79
|
+
console: z.ZodObject<{
|
|
80
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
81
|
+
logLevel: z.ZodDefault<z.ZodEnum<{
|
|
82
|
+
error: "error";
|
|
83
|
+
debug: "debug";
|
|
84
|
+
info: "info";
|
|
85
|
+
warn: "warn";
|
|
86
|
+
}>>;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
}, z.core.$strip>>;
|
|
89
|
+
private logFormatter;
|
|
90
|
+
private tracerProvider;
|
|
91
|
+
private tracer;
|
|
92
|
+
private exporter;
|
|
93
|
+
private isDisposed;
|
|
94
|
+
private spans;
|
|
95
|
+
constructor(config: BSBObservableConstructor<InstanceType<typeof Config>>);
|
|
96
|
+
init(): Promise<void>;
|
|
97
|
+
run(): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Convert BSB DTrace to OpenTelemetry trace context
|
|
100
|
+
*/
|
|
101
|
+
private getTraceContext;
|
|
102
|
+
/**
|
|
103
|
+
* Console logging fallback (Zipkin doesn't handle logs)
|
|
104
|
+
*/
|
|
105
|
+
private writeConsoleLog;
|
|
106
|
+
debug(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
|
|
107
|
+
info(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
|
|
108
|
+
warn(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void;
|
|
109
|
+
error(trace: DTrace, pluginName: string, message: string | BSBError<any>, meta?: LogMeta<any>): void;
|
|
110
|
+
createCounter(): void;
|
|
111
|
+
incrementCounter(): void;
|
|
112
|
+
createGauge(): void;
|
|
113
|
+
setGauge(): void;
|
|
114
|
+
createHistogram(): void;
|
|
115
|
+
observeHistogram(): void;
|
|
116
|
+
spanStart(trace: DTrace, pluginName: string, spanName: string, parentSpanId: string | null, attributes?: Record<string, string | number | boolean>): void;
|
|
117
|
+
spanEnd(trace: DTrace, pluginName: string, attributes?: Record<string, string | number | boolean>): void;
|
|
118
|
+
spanError(trace: DTrace, pluginName: string, error: Error, attributes?: Record<string, string | number | boolean>): void;
|
|
119
|
+
dispose(): Promise<void>;
|
|
120
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
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 = 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_trace_base_1 = require("@opentelemetry/sdk-trace-base");
|
|
69
|
+
const exporter_zipkin_1 = require("@opentelemetry/exporter-zipkin");
|
|
70
|
+
const ConfigSchema = zod_1.z.object({
|
|
71
|
+
serviceName: zod_1.z.string().default("bsb-service"),
|
|
72
|
+
serviceVersion: zod_1.z.string().optional(),
|
|
73
|
+
zipkin: zod_1.z.object({
|
|
74
|
+
url: zod_1.z.string().url().default("http://localhost:9411/api/v2/spans"),
|
|
75
|
+
headers: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
|
|
76
|
+
statusCodeTagName: zod_1.z.string().default("http.status_code"),
|
|
77
|
+
statusDescriptionTagName: zod_1.z.string().default("http.status_text"),
|
|
78
|
+
}),
|
|
79
|
+
export: zod_1.z.object({
|
|
80
|
+
maxBatchSize: zod_1.z.number().int().min(1).default(100),
|
|
81
|
+
maxQueueSize: zod_1.z.number().int().min(1).default(2048),
|
|
82
|
+
scheduledDelayMillis: zod_1.z.number().int().min(100).default(5000),
|
|
83
|
+
}),
|
|
84
|
+
resourceAttributes: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).default({}),
|
|
85
|
+
samplingRate: zod_1.z.number().min(0).max(1).default(1.0),
|
|
86
|
+
console: zod_1.z.object({
|
|
87
|
+
enabled: zod_1.z.boolean().default(true),
|
|
88
|
+
logLevel: zod_1.z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
exports.Config = (0, base_1.createConfigSchema)({
|
|
92
|
+
name: 'observable-zipkin',
|
|
93
|
+
description: 'Zipkin tracing integration for BSB framework',
|
|
94
|
+
version: '1.0.0',
|
|
95
|
+
tags: ['zipkin', 'tracing', 'observability', 'distributed-tracing'],
|
|
96
|
+
}, ConfigSchema);
|
|
97
|
+
/**
|
|
98
|
+
* Zipkin observable plugin for distributed tracing
|
|
99
|
+
*
|
|
100
|
+
* Exports traces directly to Zipkin using the Zipkin v2 API format.
|
|
101
|
+
* Provides console logging fallback for non-trace observability.
|
|
102
|
+
*/
|
|
103
|
+
class Plugin extends base_1.BSBObservable {
|
|
104
|
+
constructor(config) {
|
|
105
|
+
super(config);
|
|
106
|
+
this.logFormatter = new base_1.LogFormatter();
|
|
107
|
+
this.tracerProvider = null;
|
|
108
|
+
this.tracer = null;
|
|
109
|
+
this.exporter = null;
|
|
110
|
+
this.isDisposed = false;
|
|
111
|
+
this.spans = new Map();
|
|
112
|
+
}
|
|
113
|
+
async init() {
|
|
114
|
+
const resource = new resources_1.Resource({
|
|
115
|
+
[semantic_conventions_1.ATTR_SERVICE_NAME]: this.config.serviceName,
|
|
116
|
+
...(this.config.serviceVersion && { [semantic_conventions_1.ATTR_SERVICE_VERSION]: this.config.serviceVersion }),
|
|
117
|
+
...this.config.resourceAttributes,
|
|
118
|
+
});
|
|
119
|
+
this.exporter = new exporter_zipkin_1.ZipkinExporter({
|
|
120
|
+
url: this.config.zipkin.url,
|
|
121
|
+
headers: this.config.zipkin.headers,
|
|
122
|
+
statusCodeTagName: this.config.zipkin.statusCodeTagName,
|
|
123
|
+
statusDescriptionTagName: this.config.zipkin.statusDescriptionTagName,
|
|
124
|
+
});
|
|
125
|
+
this.tracerProvider = new sdk_trace_base_1.BasicTracerProvider({
|
|
126
|
+
resource,
|
|
127
|
+
});
|
|
128
|
+
this.tracerProvider.addSpanProcessor(new sdk_trace_base_1.BatchSpanProcessor(this.exporter, {
|
|
129
|
+
maxQueueSize: this.config.export.maxQueueSize,
|
|
130
|
+
maxExportBatchSize: this.config.export.maxBatchSize,
|
|
131
|
+
scheduledDelayMillis: this.config.export.scheduledDelayMillis,
|
|
132
|
+
}));
|
|
133
|
+
this.tracerProvider.register();
|
|
134
|
+
this.tracer = this.tracerProvider.getTracer(this.config.serviceName, this.config.serviceVersion);
|
|
135
|
+
}
|
|
136
|
+
async run() {
|
|
137
|
+
// No runtime setup needed
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Convert BSB DTrace to OpenTelemetry trace context
|
|
141
|
+
*/
|
|
142
|
+
getTraceContext(trace) {
|
|
143
|
+
const traceId = trace.t.padStart(32, "0").substring(0, 32);
|
|
144
|
+
const spanId = trace.s.padStart(16, "0").substring(0, 16);
|
|
145
|
+
const spanContext = {
|
|
146
|
+
traceId,
|
|
147
|
+
spanId,
|
|
148
|
+
traceFlags: api.TraceFlags.SAMPLED,
|
|
149
|
+
};
|
|
150
|
+
return api.trace.setSpanContext(api.context.active(), spanContext);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Console logging fallback (Zipkin doesn't handle logs)
|
|
154
|
+
*/
|
|
155
|
+
writeConsoleLog(level, trace, pluginName, message, meta) {
|
|
156
|
+
if (!this.config.console.enabled) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const formattedMessage = this.logFormatter.formatLog(trace, message, meta);
|
|
160
|
+
const timestamp = new Date().toISOString();
|
|
161
|
+
const logLine = `${timestamp} [${level.toUpperCase()}] [${pluginName}] ${formattedMessage}`;
|
|
162
|
+
const levelMap = {
|
|
163
|
+
debug: 0,
|
|
164
|
+
info: 1,
|
|
165
|
+
warn: 2,
|
|
166
|
+
error: 3,
|
|
167
|
+
};
|
|
168
|
+
const currentLevel = levelMap[this.config.console.logLevel] ?? 1;
|
|
169
|
+
const messageLevel = levelMap[level.toLowerCase()] ?? 1;
|
|
170
|
+
if (messageLevel < currentLevel) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
let func = console.log;
|
|
174
|
+
switch (level.toLowerCase()) {
|
|
175
|
+
case 'debug':
|
|
176
|
+
func = console.debug;
|
|
177
|
+
break;
|
|
178
|
+
case 'info':
|
|
179
|
+
func = console.log;
|
|
180
|
+
break;
|
|
181
|
+
case 'warn':
|
|
182
|
+
func = console.warn;
|
|
183
|
+
break;
|
|
184
|
+
case 'error':
|
|
185
|
+
func = console.error;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
func(logLine);
|
|
189
|
+
}
|
|
190
|
+
// Logging methods (fallback to console - Zipkin is trace-only)
|
|
191
|
+
debug(trace, pluginName, message, meta) {
|
|
192
|
+
if (this.mode === "production") {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.writeConsoleLog("debug", trace, pluginName, message, meta);
|
|
196
|
+
}
|
|
197
|
+
info(trace, pluginName, message, meta) {
|
|
198
|
+
this.writeConsoleLog("info", trace, pluginName, message, meta);
|
|
199
|
+
}
|
|
200
|
+
warn(trace, pluginName, message, meta) {
|
|
201
|
+
this.writeConsoleLog("warn", trace, pluginName, message, meta);
|
|
202
|
+
}
|
|
203
|
+
error(trace, pluginName, message, meta) {
|
|
204
|
+
if (message instanceof base_1.BSBError) {
|
|
205
|
+
if (message.raw !== null) {
|
|
206
|
+
this.writeConsoleLog("error", message.raw.trace, pluginName, message.raw.message, message.raw.meta);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.writeConsoleLog("error", trace, pluginName, message.message);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.writeConsoleLog("error", trace, pluginName, message, meta);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Metrics methods (no-op - Zipkin is trace-only)
|
|
217
|
+
createCounter() {
|
|
218
|
+
// Zipkin doesn't support metrics
|
|
219
|
+
}
|
|
220
|
+
incrementCounter() {
|
|
221
|
+
// Zipkin doesn't support metrics
|
|
222
|
+
}
|
|
223
|
+
createGauge() {
|
|
224
|
+
// Zipkin doesn't support metrics
|
|
225
|
+
}
|
|
226
|
+
setGauge() {
|
|
227
|
+
// Zipkin doesn't support metrics
|
|
228
|
+
}
|
|
229
|
+
createHistogram() {
|
|
230
|
+
// Zipkin doesn't support metrics
|
|
231
|
+
}
|
|
232
|
+
observeHistogram() {
|
|
233
|
+
// Zipkin doesn't support metrics
|
|
234
|
+
}
|
|
235
|
+
// Tracing methods
|
|
236
|
+
spanStart(trace, pluginName, spanName, parentSpanId, attributes) {
|
|
237
|
+
if (!this.tracer || this.isDisposed) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const context = this.getTraceContext(trace);
|
|
241
|
+
const span = this.tracer.startSpan(spanName, {
|
|
242
|
+
attributes: {
|
|
243
|
+
"bsb.plugin": pluginName,
|
|
244
|
+
"bsb.trace.t": trace.t,
|
|
245
|
+
"bsb.trace.s": trace.s,
|
|
246
|
+
...(parentSpanId ? { "bsb.parent_span_id": parentSpanId } : {}),
|
|
247
|
+
...attributes,
|
|
248
|
+
},
|
|
249
|
+
}, context);
|
|
250
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
251
|
+
this.spans.set(spanKey, span);
|
|
252
|
+
}
|
|
253
|
+
spanEnd(trace, pluginName, attributes) {
|
|
254
|
+
if (this.isDisposed) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
258
|
+
const span = this.spans.get(spanKey);
|
|
259
|
+
if (span) {
|
|
260
|
+
if (attributes) {
|
|
261
|
+
span.setAttributes(attributes);
|
|
262
|
+
}
|
|
263
|
+
span.end();
|
|
264
|
+
this.spans.delete(spanKey);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
spanError(trace, pluginName, error, attributes) {
|
|
268
|
+
if (this.isDisposed) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
272
|
+
const span = this.spans.get(spanKey);
|
|
273
|
+
if (span) {
|
|
274
|
+
span.recordException(error);
|
|
275
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
|
|
276
|
+
if (attributes) {
|
|
277
|
+
span.setAttributes(attributes);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async dispose() {
|
|
282
|
+
this.isDisposed = true;
|
|
283
|
+
for (const span of this.spans.values()) {
|
|
284
|
+
span.end();
|
|
285
|
+
}
|
|
286
|
+
this.spans.clear();
|
|
287
|
+
if (this.tracerProvider) {
|
|
288
|
+
await this.tracerProvider.shutdown();
|
|
289
|
+
this.tracerProvider = null;
|
|
290
|
+
}
|
|
291
|
+
this.tracer = null;
|
|
292
|
+
this.exporter = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
exports.Plugin = Plugin;
|
|
296
|
+
Plugin.Config = exports.Config;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginName": "observable-zipkin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"events": {},
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"serviceName": {
|
|
10
|
+
"default": "bsb-service",
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"serviceVersion": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"zipkin": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"url": {
|
|
20
|
+
"default": "http://localhost:9411/api/v2/spans",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"format": "uri"
|
|
23
|
+
},
|
|
24
|
+
"headers": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"propertyNames": {
|
|
27
|
+
"type": "string"
|
|
28
|
+
},
|
|
29
|
+
"additionalProperties": {
|
|
30
|
+
"type": "string"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"statusCodeTagName": {
|
|
34
|
+
"default": "http.status_code",
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
37
|
+
"statusDescriptionTagName": {
|
|
38
|
+
"default": "http.status_text",
|
|
39
|
+
"type": "string"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"required": [
|
|
43
|
+
"url",
|
|
44
|
+
"statusCodeTagName",
|
|
45
|
+
"statusDescriptionTagName"
|
|
46
|
+
],
|
|
47
|
+
"additionalProperties": false
|
|
48
|
+
},
|
|
49
|
+
"export": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"maxBatchSize": {
|
|
53
|
+
"default": 100,
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"minimum": 1,
|
|
56
|
+
"maximum": 9007199254740991
|
|
57
|
+
},
|
|
58
|
+
"maxQueueSize": {
|
|
59
|
+
"default": 2048,
|
|
60
|
+
"type": "integer",
|
|
61
|
+
"minimum": 1,
|
|
62
|
+
"maximum": 9007199254740991
|
|
63
|
+
},
|
|
64
|
+
"scheduledDelayMillis": {
|
|
65
|
+
"default": 5000,
|
|
66
|
+
"type": "integer",
|
|
67
|
+
"minimum": 100,
|
|
68
|
+
"maximum": 9007199254740991
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": [
|
|
72
|
+
"maxBatchSize",
|
|
73
|
+
"maxQueueSize",
|
|
74
|
+
"scheduledDelayMillis"
|
|
75
|
+
],
|
|
76
|
+
"additionalProperties": false
|
|
77
|
+
},
|
|
78
|
+
"resourceAttributes": {
|
|
79
|
+
"default": {},
|
|
80
|
+
"type": "object",
|
|
81
|
+
"propertyNames": {
|
|
82
|
+
"type": "string"
|
|
83
|
+
},
|
|
84
|
+
"additionalProperties": {
|
|
85
|
+
"type": "string"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"samplingRate": {
|
|
89
|
+
"default": 1,
|
|
90
|
+
"type": "number",
|
|
91
|
+
"minimum": 0,
|
|
92
|
+
"maximum": 1
|
|
93
|
+
},
|
|
94
|
+
"console": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {
|
|
97
|
+
"enabled": {
|
|
98
|
+
"default": true,
|
|
99
|
+
"type": "boolean"
|
|
100
|
+
},
|
|
101
|
+
"logLevel": {
|
|
102
|
+
"default": "info",
|
|
103
|
+
"type": "string",
|
|
104
|
+
"enum": [
|
|
105
|
+
"debug",
|
|
106
|
+
"info",
|
|
107
|
+
"warn",
|
|
108
|
+
"error"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"required": [
|
|
113
|
+
"enabled",
|
|
114
|
+
"logLevel"
|
|
115
|
+
],
|
|
116
|
+
"additionalProperties": false
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"required": [
|
|
120
|
+
"serviceName",
|
|
121
|
+
"zipkin",
|
|
122
|
+
"export",
|
|
123
|
+
"resourceAttributes",
|
|
124
|
+
"samplingRate",
|
|
125
|
+
"console"
|
|
126
|
+
],
|
|
127
|
+
"additionalProperties": false
|
|
128
|
+
},
|
|
129
|
+
"pluginType": "observable",
|
|
130
|
+
"capabilities": {
|
|
131
|
+
"logging": {
|
|
132
|
+
"debug": true,
|
|
133
|
+
"info": true,
|
|
134
|
+
"warn": true,
|
|
135
|
+
"error": true
|
|
136
|
+
},
|
|
137
|
+
"metrics": {
|
|
138
|
+
"createCounter": true,
|
|
139
|
+
"createGauge": true,
|
|
140
|
+
"createHistogram": true,
|
|
141
|
+
"incrementCounter": true,
|
|
142
|
+
"setGauge": true,
|
|
143
|
+
"observeHistogram": true
|
|
144
|
+
},
|
|
145
|
+
"tracing": {
|
|
146
|
+
"spanStart": true,
|
|
147
|
+
"spanEnd": true,
|
|
148
|
+
"spanError": true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "observable-zipkin",
|
|
3
|
+
"name": "observable-zipkin",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Zipkin tracing integration for BSB framework",
|
|
6
|
+
"category": "observable",
|
|
7
|
+
"tags": [
|
|
8
|
+
"zipkin",
|
|
9
|
+
"tracing",
|
|
10
|
+
"observability",
|
|
11
|
+
"distributed-tracing"
|
|
12
|
+
],
|
|
13
|
+
"documentation": [],
|
|
14
|
+
"dependencies": [],
|
|
15
|
+
"configSchema": {
|
|
16
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"serviceName": {
|
|
20
|
+
"default": "bsb-service",
|
|
21
|
+
"type": "string"
|
|
22
|
+
},
|
|
23
|
+
"serviceVersion": {
|
|
24
|
+
"type": "string"
|
|
25
|
+
},
|
|
26
|
+
"zipkin": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"url": {
|
|
30
|
+
"default": "http://localhost:9411/api/v2/spans",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"format": "uri"
|
|
33
|
+
},
|
|
34
|
+
"headers": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"propertyNames": {
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
"additionalProperties": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"statusCodeTagName": {
|
|
44
|
+
"default": "http.status_code",
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"statusDescriptionTagName": {
|
|
48
|
+
"default": "http.status_text",
|
|
49
|
+
"type": "string"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"required": [
|
|
53
|
+
"url",
|
|
54
|
+
"statusCodeTagName",
|
|
55
|
+
"statusDescriptionTagName"
|
|
56
|
+
],
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
},
|
|
59
|
+
"export": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"maxBatchSize": {
|
|
63
|
+
"default": 100,
|
|
64
|
+
"type": "integer",
|
|
65
|
+
"minimum": 1,
|
|
66
|
+
"maximum": 9007199254740991
|
|
67
|
+
},
|
|
68
|
+
"maxQueueSize": {
|
|
69
|
+
"default": 2048,
|
|
70
|
+
"type": "integer",
|
|
71
|
+
"minimum": 1,
|
|
72
|
+
"maximum": 9007199254740991
|
|
73
|
+
},
|
|
74
|
+
"scheduledDelayMillis": {
|
|
75
|
+
"default": 5000,
|
|
76
|
+
"type": "integer",
|
|
77
|
+
"minimum": 100,
|
|
78
|
+
"maximum": 9007199254740991
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"required": [
|
|
82
|
+
"maxBatchSize",
|
|
83
|
+
"maxQueueSize",
|
|
84
|
+
"scheduledDelayMillis"
|
|
85
|
+
],
|
|
86
|
+
"additionalProperties": false
|
|
87
|
+
},
|
|
88
|
+
"resourceAttributes": {
|
|
89
|
+
"default": {},
|
|
90
|
+
"type": "object",
|
|
91
|
+
"propertyNames": {
|
|
92
|
+
"type": "string"
|
|
93
|
+
},
|
|
94
|
+
"additionalProperties": {
|
|
95
|
+
"type": "string"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"samplingRate": {
|
|
99
|
+
"default": 1,
|
|
100
|
+
"type": "number",
|
|
101
|
+
"minimum": 0,
|
|
102
|
+
"maximum": 1
|
|
103
|
+
},
|
|
104
|
+
"console": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"enabled": {
|
|
108
|
+
"default": true,
|
|
109
|
+
"type": "boolean"
|
|
110
|
+
},
|
|
111
|
+
"logLevel": {
|
|
112
|
+
"default": "info",
|
|
113
|
+
"type": "string",
|
|
114
|
+
"enum": [
|
|
115
|
+
"debug",
|
|
116
|
+
"info",
|
|
117
|
+
"warn",
|
|
118
|
+
"error"
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"required": [
|
|
123
|
+
"enabled",
|
|
124
|
+
"logLevel"
|
|
125
|
+
],
|
|
126
|
+
"additionalProperties": false
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"required": [
|
|
130
|
+
"serviceName",
|
|
131
|
+
"zipkin",
|
|
132
|
+
"export",
|
|
133
|
+
"resourceAttributes",
|
|
134
|
+
"samplingRate",
|
|
135
|
+
"console"
|
|
136
|
+
],
|
|
137
|
+
"additionalProperties": false
|
|
138
|
+
}
|
|
139
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bsb/observable-zipkin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zipkin tracing integration for BSB framework",
|
|
5
|
+
"main": "lib/plugins/observable-zipkin/index.js",
|
|
6
|
+
"types": "lib/plugins/observable-zipkin/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"clean": "bsb-plugin-cli clean",
|
|
9
|
+
"build": "bsb-plugin-cli build",
|
|
10
|
+
"start": "bsb-plugin-cli start",
|
|
11
|
+
"dev": "bsb-plugin-cli dev",
|
|
12
|
+
"test": "bsb-plugin-cli test",
|
|
13
|
+
"publish:client": "bsb client publish",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"bsb",
|
|
18
|
+
"zipkin",
|
|
19
|
+
"tracing",
|
|
20
|
+
"observability",
|
|
21
|
+
"distributed-tracing",
|
|
22
|
+
"opentelemetry"
|
|
23
|
+
],
|
|
24
|
+
"license": "(AGPL-3.0-only OR Commercial)",
|
|
25
|
+
"author": {
|
|
26
|
+
"name": "BetterCorp (PTY) Ltd",
|
|
27
|
+
"email": "nick@bettercorp.dev",
|
|
28
|
+
"url": "https://bettercorp.dev/"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@bsb/base": "^9.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@opentelemetry/api": "^1.9.0",
|
|
35
|
+
"@opentelemetry/exporter-zipkin": "^1.28.0",
|
|
36
|
+
"@opentelemetry/resources": "^1.28.0",
|
|
37
|
+
"@opentelemetry/sdk-trace-base": "^1.28.0",
|
|
38
|
+
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
39
|
+
"zod": "^4.3.6"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@bsb/base": "file:../../../nodejs",
|
|
43
|
+
"@types/node": "^22.10.5",
|
|
44
|
+
"rimraf": "^6.0.1",
|
|
45
|
+
"typescript": "^5.7.3"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://io.bsbcode.dev/plugins/bsb/observable-zipkin"
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
|
|
28
|
+
import {
|
|
29
|
+
BSBObservable,
|
|
30
|
+
BSBObservableConstructor,
|
|
31
|
+
createConfigSchema,
|
|
32
|
+
LogFormatter,
|
|
33
|
+
BSBError
|
|
34
|
+
} from "@bsb/base";
|
|
35
|
+
import { DTrace, LogMeta } from "@bsb/base";
|
|
36
|
+
import { z } from "zod";
|
|
37
|
+
import * as api from "@opentelemetry/api";
|
|
38
|
+
import { Resource } from "@opentelemetry/resources";
|
|
39
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
40
|
+
import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
41
|
+
import { ZipkinExporter } from "@opentelemetry/exporter-zipkin";
|
|
42
|
+
|
|
43
|
+
const ConfigSchema = z.object({
|
|
44
|
+
serviceName: z.string().default("bsb-service"),
|
|
45
|
+
serviceVersion: z.string().optional(),
|
|
46
|
+
|
|
47
|
+
zipkin: z.object({
|
|
48
|
+
url: z.string().url().default("http://localhost:9411/api/v2/spans"),
|
|
49
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
50
|
+
statusCodeTagName: z.string().default("http.status_code"),
|
|
51
|
+
statusDescriptionTagName: z.string().default("http.status_text"),
|
|
52
|
+
}),
|
|
53
|
+
|
|
54
|
+
export: z.object({
|
|
55
|
+
maxBatchSize: z.number().int().min(1).default(100),
|
|
56
|
+
maxQueueSize: z.number().int().min(1).default(2048),
|
|
57
|
+
scheduledDelayMillis: z.number().int().min(100).default(5000),
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
resourceAttributes: z.record(z.string(), z.string()).default({}),
|
|
61
|
+
samplingRate: z.number().min(0).max(1).default(1.0),
|
|
62
|
+
|
|
63
|
+
console: z.object({
|
|
64
|
+
enabled: z.boolean().default(true),
|
|
65
|
+
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const Config = createConfigSchema(
|
|
70
|
+
{
|
|
71
|
+
name: 'observable-zipkin',
|
|
72
|
+
description: 'Zipkin tracing integration for BSB framework',
|
|
73
|
+
version: '1.0.0',
|
|
74
|
+
tags: ['zipkin', 'tracing', 'observability', 'distributed-tracing'],
|
|
75
|
+
},
|
|
76
|
+
ConfigSchema
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Zipkin observable plugin for distributed tracing
|
|
81
|
+
*
|
|
82
|
+
* Exports traces directly to Zipkin using the Zipkin v2 API format.
|
|
83
|
+
* Provides console logging fallback for non-trace observability.
|
|
84
|
+
*/
|
|
85
|
+
export class Plugin extends BSBObservable<InstanceType<typeof Config>> {
|
|
86
|
+
static Config = Config;
|
|
87
|
+
|
|
88
|
+
private logFormatter = new LogFormatter();
|
|
89
|
+
private tracerProvider: BasicTracerProvider | null = null;
|
|
90
|
+
private tracer: api.Tracer | null = null;
|
|
91
|
+
private exporter: ZipkinExporter | null = null;
|
|
92
|
+
private isDisposed = false;
|
|
93
|
+
|
|
94
|
+
private spans = new Map<string, api.Span>();
|
|
95
|
+
|
|
96
|
+
constructor(config: BSBObservableConstructor<InstanceType<typeof Config>>) {
|
|
97
|
+
super(config);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public async init(): Promise<void> {
|
|
101
|
+
const resource = new Resource({
|
|
102
|
+
[ATTR_SERVICE_NAME]: this.config.serviceName,
|
|
103
|
+
...(this.config.serviceVersion && { [ATTR_SERVICE_VERSION]: this.config.serviceVersion }),
|
|
104
|
+
...this.config.resourceAttributes,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.exporter = new ZipkinExporter({
|
|
108
|
+
url: this.config.zipkin.url,
|
|
109
|
+
headers: this.config.zipkin.headers,
|
|
110
|
+
statusCodeTagName: this.config.zipkin.statusCodeTagName,
|
|
111
|
+
statusDescriptionTagName: this.config.zipkin.statusDescriptionTagName,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
this.tracerProvider = new BasicTracerProvider({
|
|
115
|
+
resource,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.tracerProvider.addSpanProcessor(
|
|
119
|
+
new BatchSpanProcessor(this.exporter, {
|
|
120
|
+
maxQueueSize: this.config.export.maxQueueSize,
|
|
121
|
+
maxExportBatchSize: this.config.export.maxBatchSize,
|
|
122
|
+
scheduledDelayMillis: this.config.export.scheduledDelayMillis,
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
this.tracerProvider.register();
|
|
127
|
+
|
|
128
|
+
this.tracer = this.tracerProvider.getTracer(
|
|
129
|
+
this.config.serviceName,
|
|
130
|
+
this.config.serviceVersion
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public async run(): Promise<void> {
|
|
135
|
+
// No runtime setup needed
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Convert BSB DTrace to OpenTelemetry trace context
|
|
140
|
+
*/
|
|
141
|
+
private getTraceContext(trace: DTrace): api.Context {
|
|
142
|
+
const traceId = trace.t.padStart(32, "0").substring(0, 32);
|
|
143
|
+
const spanId = trace.s.padStart(16, "0").substring(0, 16);
|
|
144
|
+
|
|
145
|
+
const spanContext: api.SpanContext = {
|
|
146
|
+
traceId,
|
|
147
|
+
spanId,
|
|
148
|
+
traceFlags: api.TraceFlags.SAMPLED,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return api.trace.setSpanContext(api.context.active(), spanContext);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Console logging fallback (Zipkin doesn't handle logs)
|
|
156
|
+
*/
|
|
157
|
+
private writeConsoleLog(
|
|
158
|
+
level: string,
|
|
159
|
+
trace: DTrace,
|
|
160
|
+
pluginName: string,
|
|
161
|
+
message: string,
|
|
162
|
+
meta?: LogMeta<any>
|
|
163
|
+
): void {
|
|
164
|
+
if (!this.config.console.enabled) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const formattedMessage = this.logFormatter.formatLog(trace, message, meta);
|
|
169
|
+
const timestamp = new Date().toISOString();
|
|
170
|
+
const logLine = `${timestamp} [${level.toUpperCase()}] [${pluginName}] ${formattedMessage}`;
|
|
171
|
+
|
|
172
|
+
const levelMap = {
|
|
173
|
+
debug: 0,
|
|
174
|
+
info: 1,
|
|
175
|
+
warn: 2,
|
|
176
|
+
error: 3,
|
|
177
|
+
} as const;
|
|
178
|
+
|
|
179
|
+
const currentLevel = levelMap[this.config.console.logLevel as keyof typeof levelMap] ?? 1;
|
|
180
|
+
const messageLevel = levelMap[level.toLowerCase() as keyof typeof levelMap] ?? 1;
|
|
181
|
+
|
|
182
|
+
if (messageLevel < currentLevel) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type ConsoleMethod = typeof console.debug | typeof console.log | typeof console.warn | typeof console.error;
|
|
187
|
+
let func: ConsoleMethod = console.log;
|
|
188
|
+
|
|
189
|
+
switch (level.toLowerCase()) {
|
|
190
|
+
case 'debug':
|
|
191
|
+
func = console.debug;
|
|
192
|
+
break;
|
|
193
|
+
case 'info':
|
|
194
|
+
func = console.log;
|
|
195
|
+
break;
|
|
196
|
+
case 'warn':
|
|
197
|
+
func = console.warn;
|
|
198
|
+
break;
|
|
199
|
+
case 'error':
|
|
200
|
+
func = console.error;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func(logLine);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Logging methods (fallback to console - Zipkin is trace-only)
|
|
208
|
+
public debug(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void {
|
|
209
|
+
if (this.mode === "production") {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.writeConsoleLog("debug", trace, pluginName, message, meta);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public info(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void {
|
|
216
|
+
this.writeConsoleLog("info", trace, pluginName, message, meta);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
public warn(trace: DTrace, pluginName: string, message: string, meta: LogMeta<any>): void {
|
|
220
|
+
this.writeConsoleLog("warn", trace, pluginName, message, meta);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public error(trace: DTrace, pluginName: string, message: string | BSBError<any>, meta?: LogMeta<any>): void {
|
|
224
|
+
if (message instanceof BSBError) {
|
|
225
|
+
if (message.raw !== null) {
|
|
226
|
+
this.writeConsoleLog("error", message.raw.trace, pluginName, message.raw.message, message.raw.meta);
|
|
227
|
+
} else {
|
|
228
|
+
this.writeConsoleLog("error", trace, pluginName, message.message);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
this.writeConsoleLog("error", trace, pluginName, message, meta);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Metrics methods (no-op - Zipkin is trace-only)
|
|
236
|
+
public createCounter(): void {
|
|
237
|
+
// Zipkin doesn't support metrics
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public incrementCounter(): void {
|
|
241
|
+
// Zipkin doesn't support metrics
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public createGauge(): void {
|
|
245
|
+
// Zipkin doesn't support metrics
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public setGauge(): void {
|
|
249
|
+
// Zipkin doesn't support metrics
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public createHistogram(): void {
|
|
253
|
+
// Zipkin doesn't support metrics
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public observeHistogram(): void {
|
|
257
|
+
// Zipkin doesn't support metrics
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Tracing methods
|
|
261
|
+
public spanStart(
|
|
262
|
+
trace: DTrace,
|
|
263
|
+
pluginName: string,
|
|
264
|
+
spanName: string,
|
|
265
|
+
parentSpanId: string | null,
|
|
266
|
+
attributes?: Record<string, string | number | boolean>
|
|
267
|
+
): void {
|
|
268
|
+
if (!this.tracer || this.isDisposed) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const context = this.getTraceContext(trace);
|
|
273
|
+
const span = this.tracer.startSpan(
|
|
274
|
+
spanName,
|
|
275
|
+
{
|
|
276
|
+
attributes: {
|
|
277
|
+
"bsb.plugin": pluginName,
|
|
278
|
+
"bsb.trace.t": trace.t,
|
|
279
|
+
"bsb.trace.s": trace.s,
|
|
280
|
+
...(parentSpanId ? { "bsb.parent_span_id": parentSpanId } : {}),
|
|
281
|
+
...attributes,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
context
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
288
|
+
this.spans.set(spanKey, span);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public spanEnd(
|
|
292
|
+
trace: DTrace,
|
|
293
|
+
pluginName: string,
|
|
294
|
+
attributes?: Record<string, string | number | boolean>
|
|
295
|
+
): void {
|
|
296
|
+
if (this.isDisposed) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
301
|
+
const span = this.spans.get(spanKey);
|
|
302
|
+
if (span) {
|
|
303
|
+
if (attributes) {
|
|
304
|
+
span.setAttributes(attributes);
|
|
305
|
+
}
|
|
306
|
+
span.end();
|
|
307
|
+
this.spans.delete(spanKey);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
public spanError(
|
|
312
|
+
trace: DTrace,
|
|
313
|
+
pluginName: string,
|
|
314
|
+
error: Error,
|
|
315
|
+
attributes?: Record<string, string | number | boolean>
|
|
316
|
+
): void {
|
|
317
|
+
if (this.isDisposed) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const spanKey = `${trace.t}:${trace.s}`;
|
|
322
|
+
const span = this.spans.get(spanKey);
|
|
323
|
+
if (span) {
|
|
324
|
+
span.recordException(error);
|
|
325
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
|
|
326
|
+
if (attributes) {
|
|
327
|
+
span.setAttributes(attributes);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public async dispose(): Promise<void> {
|
|
333
|
+
this.isDisposed = true;
|
|
334
|
+
|
|
335
|
+
for (const span of this.spans.values()) {
|
|
336
|
+
span.end();
|
|
337
|
+
}
|
|
338
|
+
this.spans.clear();
|
|
339
|
+
|
|
340
|
+
if (this.tracerProvider) {
|
|
341
|
+
await this.tracerProvider.shutdown();
|
|
342
|
+
this.tracerProvider = null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
this.tracer = null;
|
|
346
|
+
this.exporter = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./lib",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"types": ["node"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "lib"]
|
|
19
|
+
}
|