@logtape/otel 1.3.0-dev.380 → 1.3.0-dev.382
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 +57 -6
- package/deno.json +1 -1
- package/dist/deno.cjs +1 -1
- package/dist/deno.js +1 -1
- package/dist/deno.js.map +1 -1
- package/dist/mod.cjs +27 -0
- package/dist/mod.d.cts.map +1 -1
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +28 -1
- package/dist/mod.js.map +1 -1
- package/package.json +2 -2
- package/src/mod.test.ts +54 -0
- package/src/mod.ts +50 -0
package/README.md
CHANGED
|
@@ -84,25 +84,26 @@ Or you can even pass an existing OpenTelemetry [`LoggerProvider`] instance:
|
|
|
84
84
|
~~~~ typescript
|
|
85
85
|
import { configure } from "@logtape/logtape";
|
|
86
86
|
import { getOpenTelemetrySink } from "@logtape/otel";
|
|
87
|
-
import { OTLPLogExporter } from
|
|
87
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
88
88
|
import {
|
|
89
89
|
LoggerProvider,
|
|
90
90
|
SimpleLogRecordProcessor,
|
|
91
|
-
} from
|
|
91
|
+
} from "@opentelemetry/sdk-logs";
|
|
92
92
|
|
|
93
93
|
const exporter = new OTLPLogExporter({
|
|
94
|
-
url: "https://my-otel-collector:
|
|
94
|
+
url: "https://my-otel-collector:4318/v1/logs",
|
|
95
95
|
headers: { "x-api-key": "my-api-key" },
|
|
96
96
|
});
|
|
97
|
-
const loggerProvider = new LoggerProvider(
|
|
98
|
-
|
|
97
|
+
const loggerProvider = new LoggerProvider({
|
|
98
|
+
processors: [new SimpleLogRecordProcessor(exporter)],
|
|
99
|
+
});
|
|
99
100
|
|
|
100
101
|
await configure({
|
|
101
102
|
sinks: {
|
|
102
103
|
otel: getOpenTelemetrySink({ loggerProvider }),
|
|
103
104
|
},
|
|
104
105
|
loggers: [
|
|
105
|
-
{ category: [], sinks: ["otel"],
|
|
106
|
+
{ category: [], sinks: ["otel"], lowestLevel: "debug" },
|
|
106
107
|
],
|
|
107
108
|
});
|
|
108
109
|
~~~~
|
|
@@ -115,6 +116,56 @@ function and [`OpenTelemetrySinkOptions`] type.
|
|
|
115
116
|
[`LoggerProvider`]: https://open-telemetry.github.io/opentelemetry-js/classes/_opentelemetry_sdk_logs.LoggerProvider.html
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
Protocol selection
|
|
120
|
+
------------------
|
|
121
|
+
|
|
122
|
+
By default, the sink uses the `http/json` protocol to send logs to the
|
|
123
|
+
OpenTelemetry collector. You can change the protocol by setting the
|
|
124
|
+
`OTEL_EXPORTER_OTLP_PROTOCOL` or `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` environment
|
|
125
|
+
variable:
|
|
126
|
+
|
|
127
|
+
~~~~ bash
|
|
128
|
+
# Use gRPC protocol (server environments only)
|
|
129
|
+
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
|
130
|
+
|
|
131
|
+
# Use HTTP/Protobuf protocol
|
|
132
|
+
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
|
133
|
+
|
|
134
|
+
# Use HTTP/JSON protocol (default)
|
|
135
|
+
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
|
|
136
|
+
~~~~
|
|
137
|
+
|
|
138
|
+
The available protocols are:
|
|
139
|
+
|
|
140
|
+
- `grpc`: Uses gRPC transport. This is only available in server environments
|
|
141
|
+
(Node.js, Deno, Bun) and not in browsers due to gRPC's HTTP/2 requirement.
|
|
142
|
+
- `http/protobuf`: Uses HTTP transport with Protocol Buffers encoding.
|
|
143
|
+
Works in all environments.
|
|
144
|
+
- `http/json`: Uses HTTP transport with JSON encoding. Works in all
|
|
145
|
+
environments. This is the default.
|
|
146
|
+
|
|
147
|
+
> [!NOTE]
|
|
148
|
+
> When using gRPC protocol, make sure to use the correct port (typically 4317
|
|
149
|
+
> for gRPC vs 4318 for HTTP).
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
No-op fallback
|
|
153
|
+
--------------
|
|
154
|
+
|
|
155
|
+
If no OTLP endpoint is configured (neither via options nor environment
|
|
156
|
+
variables), the sink automatically falls back to a no-op logger that discards
|
|
157
|
+
all log records. This prevents errors when running in environments where
|
|
158
|
+
OpenTelemetry is not set up.
|
|
159
|
+
|
|
160
|
+
The sink checks for endpoints in the following order:
|
|
161
|
+
|
|
162
|
+
1. `otlpExporterConfig.url` option
|
|
163
|
+
2. `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` environment variable
|
|
164
|
+
3. `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable
|
|
165
|
+
|
|
166
|
+
If none of these are set, the no-op fallback is used.
|
|
167
|
+
|
|
168
|
+
|
|
118
169
|
Diagnostic logging
|
|
119
170
|
------------------
|
|
120
171
|
|
package/deno.json
CHANGED
package/dist/deno.cjs
CHANGED
package/dist/deno.js
CHANGED
package/dist/deno.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deno.js","names":[],"sources":["../deno.json"],"sourcesContent":["{\n \"name\": \"@logtape/otel\",\n \"version\": \"1.3.0-dev.
|
|
1
|
+
{"version":3,"file":"deno.js","names":[],"sources":["../deno.json"],"sourcesContent":["{\n \"name\": \"@logtape/otel\",\n \"version\": \"1.3.0-dev.382+d8530ecb\",\n \"license\": \"MIT\",\n \"exports\": {\n \".\": \"./src/mod.ts\"\n },\n \"imports\": {\n \"@opentelemetry/api\": \"npm:@opentelemetry/api@^1.9.0\",\n \"@opentelemetry/api-logs\": \"npm:@opentelemetry/api-logs@^0.208.0\",\n \"@opentelemetry/exporter-logs-otlp-grpc\": \"npm:@opentelemetry/exporter-logs-otlp-grpc@^0.208.0\",\n \"@opentelemetry/exporter-logs-otlp-http\": \"npm:@opentelemetry/exporter-logs-otlp-http@^0.208.0\",\n \"@opentelemetry/exporter-logs-otlp-proto\": \"npm:@opentelemetry/exporter-logs-otlp-proto@^0.208.0\",\n \"@opentelemetry/otlp-exporter-base\": \"npm:@opentelemetry/otlp-exporter-base@^0.208.0\",\n \"@opentelemetry/resources\": \"npm:@opentelemetry/resources@^2.2.0\",\n \"@opentelemetry/sdk-logs\": \"npm:@opentelemetry/sdk-logs@^0.208.0\",\n \"@opentelemetry/semantic-conventions\": \"npm:@opentelemetry/semantic-conventions@^1.38.0\"\n },\n \"tasks\": {\n \"build\": \"pnpm build\",\n \"test\": \"deno test --allow-net --allow-env\"\n }\n}\n"],"mappings":";WACU;cACG;cACA;cACA,EACT,KAAK,eACN;cACU;CACT,sBAAsB;CACtB,2BAA2B;CAC3B,0CAA0C;CAC1C,0CAA0C;CAC1C,2CAA2C;CAC3C,qCAAqC;CACrC,4BAA4B;CAC5B,2BAA2B;CAC3B,uCAAuC;AACxC;YACQ;CACP,SAAS;CACT,QAAQ;AACT;mBArBH;;;;;;;AAsBC"}
|
package/dist/mod.cjs
CHANGED
|
@@ -23,6 +23,23 @@ function getEnvironmentVariable(name) {
|
|
|
23
23
|
return void 0;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
+
* Checks if an OTLP endpoint is configured via environment variables or options.
|
|
27
|
+
* Checks the following environment variables:
|
|
28
|
+
* - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (logs-specific endpoint)
|
|
29
|
+
* - `OTEL_EXPORTER_OTLP_ENDPOINT` (general OTLP endpoint)
|
|
30
|
+
*
|
|
31
|
+
* @param config Optional exporter configuration that may contain a URL.
|
|
32
|
+
* @returns `true` if an endpoint is configured, `false` otherwise.
|
|
33
|
+
*/
|
|
34
|
+
function hasOtlpEndpoint(config) {
|
|
35
|
+
if (config?.url) return true;
|
|
36
|
+
const logsEndpoint = getEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT");
|
|
37
|
+
if (logsEndpoint) return true;
|
|
38
|
+
const endpoint = getEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
|
|
39
|
+
if (endpoint) return true;
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
26
43
|
* Detects the OTLP protocol from environment variables.
|
|
27
44
|
* Priority:
|
|
28
45
|
* 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`
|
|
@@ -63,12 +80,22 @@ async function createOtlpExporter(config) {
|
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
/**
|
|
83
|
+
* A no-op logger provider that returns NOOP_LOGGER for all requests.
|
|
84
|
+
* Used when no OTLP endpoint is configured to avoid repeated connection errors.
|
|
85
|
+
*/
|
|
86
|
+
const noopLoggerProvider = { getLogger: () => __opentelemetry_api_logs.NOOP_LOGGER };
|
|
87
|
+
/**
|
|
66
88
|
* Initializes the logger provider asynchronously.
|
|
67
89
|
* This is used when the user doesn't provide a custom logger provider.
|
|
90
|
+
*
|
|
91
|
+
* If no OTLP endpoint is configured (via options or environment variables),
|
|
92
|
+
* returns a noop logger provider to avoid repeated connection errors.
|
|
93
|
+
*
|
|
68
94
|
* @param options The exporter options.
|
|
69
95
|
* @returns A promise that resolves to the initialized logger provider.
|
|
70
96
|
*/
|
|
71
97
|
async function initializeLoggerProvider(options) {
|
|
98
|
+
if (!hasOtlpEndpoint(options.otlpExporterConfig)) return noopLoggerProvider;
|
|
72
99
|
const resource = (0, __opentelemetry_resources.defaultResource)().merge((0, __opentelemetry_resources.resourceFromAttributes)({ [__opentelemetry_semantic_conventions.ATTR_SERVICE_NAME]: options.serviceName ?? getEnvironmentVariable("OTEL_SERVICE_NAME") }));
|
|
73
100
|
const otlpExporter = await createOtlpExporter(options.otlpExporterConfig);
|
|
74
101
|
const loggerProvider = new __opentelemetry_sdk_logs.LoggerProvider({
|
package/dist/mod.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.cts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"mod.d.cts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAeoF;KAoJ/E,eAAA,GAAkB,cAAH,GAAA;EAAA;;AAOM;AAS1B;AAAgD;AAQhD;EAAyB,QAAA,CAAA,EAAA,GAAA,GAjBN,OAiBM,CAAA,IAAA,CAAA;CAAA;;AAAiC;AAM1D;AAAkE;;;AAuB/C,KArCP,cAAA,GAqCO,MAAA,GAAA,SAAA;AAAc,KAnC5B,OAAA,GAmC4B,CAAA,MAAA,GAAA,IAAA,GAAA,SAAA,CAAA,EAAA;AAmBjC;;;;AACsC,KAjD1B,aAAA,GAiD0B,CAAA,OAAA,EAjDA,OAiDA,EAAA,GAjDY,QAiDZ;AAkBtC;;;;AACsC,KA9D1B,YAAA,GA8D0B,MAAA,GAAA,eAAA,GAAA,WAAA;AAoDtC;;;UA7GU,4BAAA,CA+GN;EAAgC;AA2FpC;;;;;AAEyB;;;qCAlMY;;;;;;;mBAQlB;;;;;;;;;;;;;;;;;UAmBF,gCAAA,SACP;;;;kBAIQ;;;;;;;;;;;;;UAcD,gCAAA,SACP;;;;;;;;;uBAUa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0CX,wBAAA,GACR,mCACA;;;;;;;;;;;iBA2FY,oBAAA,WACL,2BACR,OAAO"}
|
package/dist/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"mod.d.ts","names":[],"sources":["../src/mod.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAeoF;KAoJ/E,eAAA,GAAkB,cAAH,GAAA;EAAA;;AAOM;AAS1B;AAAgD;AAQhD;EAAyB,QAAA,CAAA,EAAA,GAAA,GAjBN,OAiBM,CAAA,IAAA,CAAA;CAAA;;AAAiC;AAM1D;AAAkE;;;AAuB/C,KArCP,cAAA,GAqCO,MAAA,GAAA,SAAA;AAAc,KAnC5B,OAAA,GAmC4B,CAAA,MAAA,GAAA,IAAA,GAAA,SAAA,CAAA,EAAA;AAmBjC;;;;AACsC,KAjD1B,aAAA,GAiD0B,CAAA,OAAA,EAjDA,OAiDA,EAAA,GAjDY,QAiDZ;AAkBtC;;;;AACsC,KA9D1B,YAAA,GA8D0B,MAAA,GAAA,eAAA,GAAA,WAAA;AAoDtC;;;UA7GU,4BAAA,CA+GN;EAAgC;AA2FpC;;;;;AAEyB;;;qCAlMY;;;;;;;mBAQlB;;;;;;;;;;;;;;;;;UAmBF,gCAAA,SACP;;;;kBAIQ;;;;;;;;;;;;;UAcD,gCAAA,SACP;;;;;;;;;uBAUa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA0CX,wBAAA,GACR,mCACA;;;;;;;;;;;iBA2FY,oBAAA,WACL,2BACR,OAAO"}
|
package/dist/mod.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import deno_default from "./deno.js";
|
|
2
2
|
import { getLogger } from "@logtape/logtape";
|
|
3
3
|
import { DiagLogLevel, diag } from "@opentelemetry/api";
|
|
4
|
-
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
4
|
+
import { NOOP_LOGGER, SeverityNumber } from "@opentelemetry/api-logs";
|
|
5
5
|
import { defaultResource, resourceFromAttributes } from "@opentelemetry/resources";
|
|
6
6
|
import { LoggerProvider as LoggerProvider$1, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
7
7
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
@@ -22,6 +22,23 @@ function getEnvironmentVariable(name) {
|
|
|
22
22
|
return void 0;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
+
* Checks if an OTLP endpoint is configured via environment variables or options.
|
|
26
|
+
* Checks the following environment variables:
|
|
27
|
+
* - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (logs-specific endpoint)
|
|
28
|
+
* - `OTEL_EXPORTER_OTLP_ENDPOINT` (general OTLP endpoint)
|
|
29
|
+
*
|
|
30
|
+
* @param config Optional exporter configuration that may contain a URL.
|
|
31
|
+
* @returns `true` if an endpoint is configured, `false` otherwise.
|
|
32
|
+
*/
|
|
33
|
+
function hasOtlpEndpoint(config) {
|
|
34
|
+
if (config?.url) return true;
|
|
35
|
+
const logsEndpoint = getEnvironmentVariable("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT");
|
|
36
|
+
if (logsEndpoint) return true;
|
|
37
|
+
const endpoint = getEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
|
|
38
|
+
if (endpoint) return true;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
25
42
|
* Detects the OTLP protocol from environment variables.
|
|
26
43
|
* Priority:
|
|
27
44
|
* 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`
|
|
@@ -62,12 +79,22 @@ async function createOtlpExporter(config) {
|
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
81
|
/**
|
|
82
|
+
* A no-op logger provider that returns NOOP_LOGGER for all requests.
|
|
83
|
+
* Used when no OTLP endpoint is configured to avoid repeated connection errors.
|
|
84
|
+
*/
|
|
85
|
+
const noopLoggerProvider = { getLogger: () => NOOP_LOGGER };
|
|
86
|
+
/**
|
|
65
87
|
* Initializes the logger provider asynchronously.
|
|
66
88
|
* This is used when the user doesn't provide a custom logger provider.
|
|
89
|
+
*
|
|
90
|
+
* If no OTLP endpoint is configured (via options or environment variables),
|
|
91
|
+
* returns a noop logger provider to avoid repeated connection errors.
|
|
92
|
+
*
|
|
67
93
|
* @param options The exporter options.
|
|
68
94
|
* @returns A promise that resolves to the initialized logger provider.
|
|
69
95
|
*/
|
|
70
96
|
async function initializeLoggerProvider(options) {
|
|
97
|
+
if (!hasOtlpEndpoint(options.otlpExporterConfig)) return noopLoggerProvider;
|
|
71
98
|
const resource = defaultResource().merge(resourceFromAttributes({ [ATTR_SERVICE_NAME]: options.serviceName ?? getEnvironmentVariable("OTEL_SERVICE_NAME") }));
|
|
72
99
|
const otlpExporter = await createOtlpExporter(options.otlpExporterConfig);
|
|
73
100
|
const loggerProvider = new LoggerProvider$1({
|
package/dist/mod.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.js","names":["name: string","config?: OTLPExporterNodeConfigBase","options: OpenTelemetrySinkExporterOptions","LoggerProvider","logger: OTLogger","record: LogRecord","options: OpenTelemetrySinkOptions","loggerProvider","logger","metadata","sink: Sink & AsyncDisposable","sink","loggerProvider: ILoggerProvider | null","logger: OTLogger | null","initPromise: Promise<void> | null","initError: Error | null","level: string","properties: Record<string, unknown>","objectRenderer: ObjectRenderer","attributes: Record<string, AnyValue>","v","value: unknown","message: readonly unknown[]","body: (string | null | undefined)[]","bodyFormatter: BodyFormatter","inspect: (value: unknown) => string","msg: string","#escape"],"sources":["../src/mod.ts"],"sourcesContent":["import {\n getLogger,\n type Logger,\n type LogRecord,\n type Sink,\n} from \"@logtape/logtape\";\nimport { diag, type DiagLogger, DiagLogLevel } from \"@opentelemetry/api\";\nimport {\n type AnyValue,\n type Logger as OTLogger,\n type LoggerProvider as LoggerProviderBase,\n type LogRecord as OTLogRecord,\n SeverityNumber,\n} from \"@opentelemetry/api-logs\";\nimport type { OTLPExporterNodeConfigBase } from \"@opentelemetry/otlp-exporter-base\";\nimport {\n defaultResource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport {\n LoggerProvider,\n SimpleLogRecordProcessor,\n} from \"@opentelemetry/sdk-logs\";\nimport { ATTR_SERVICE_NAME } from \"@opentelemetry/semantic-conventions\";\nimport metadata from \"../deno.json\" with { type: \"json\" };\n\n/**\n * Gets an environment variable value across different JavaScript runtimes.\n * @param name The environment variable name.\n * @returns The environment variable value, or undefined if not found.\n */\nfunction getEnvironmentVariable(name: string): string | undefined {\n // Deno runtime\n if (typeof Deno !== \"undefined\" && Deno.env) {\n try {\n return Deno.env.get(name);\n } catch {\n // Deno.env.get() can throw if permissions are not granted\n return undefined;\n }\n }\n\n // Node.js/Bun runtime\n if (\n typeof globalThis !== \"undefined\" && \"process\" in globalThis &&\n // @ts-ignore: process exists in Node.js/Bun\n typeof globalThis.process !== \"undefined\" &&\n // @ts-ignore: process.env exists in Node.js/Bun\n typeof globalThis.process.env === \"object\" &&\n // @ts-ignore: process.env exists in Node.js/Bun\n globalThis.process.env !== null\n ) {\n // @ts-ignore: process.env exists in Node.js/Bun\n return globalThis.process.env[name];\n }\n\n // Browser/other environments - no environment variables available\n return undefined;\n}\n\n/**\n * Detects the OTLP protocol from environment variables.\n * Priority:\n * 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`\n * 2. `OTEL_EXPORTER_OTLP_PROTOCOL`\n * 3. Default: `\"http/json\"` (for backward compatibility)\n *\n * @returns The detected OTLP protocol.\n */\nfunction detectOtlpProtocol(): OtlpProtocol {\n const logsProtocol = getEnvironmentVariable(\n \"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL\",\n );\n if (\n logsProtocol === \"grpc\" ||\n logsProtocol === \"http/protobuf\" ||\n logsProtocol === \"http/json\"\n ) {\n return logsProtocol;\n }\n\n const protocol = getEnvironmentVariable(\"OTEL_EXPORTER_OTLP_PROTOCOL\");\n if (\n protocol === \"grpc\" ||\n protocol === \"http/protobuf\" ||\n protocol === \"http/json\"\n ) {\n return protocol;\n }\n\n return \"http/json\";\n}\n\n/**\n * Creates an OTLP log exporter based on the detected protocol.\n * Uses dynamic imports to maintain browser compatibility when gRPC is not used.\n * @param config Optional exporter configuration.\n * @returns A promise that resolves to the appropriate OTLP log exporter.\n */\nasync function createOtlpExporter(\n config?: OTLPExporterNodeConfigBase,\n // deno-lint-ignore no-explicit-any\n): Promise<any> {\n const protocol = detectOtlpProtocol();\n\n switch (protocol) {\n case \"grpc\": {\n const { OTLPLogExporter } = await import(\n \"@opentelemetry/exporter-logs-otlp-grpc\"\n );\n return new OTLPLogExporter(config);\n }\n case \"http/protobuf\": {\n const { OTLPLogExporter } = await import(\n \"@opentelemetry/exporter-logs-otlp-proto\"\n );\n return new OTLPLogExporter(config);\n }\n case \"http/json\":\n default: {\n const { OTLPLogExporter } = await import(\n \"@opentelemetry/exporter-logs-otlp-http\"\n );\n return new OTLPLogExporter(config);\n }\n }\n}\n\n/**\n * The OpenTelemetry logger provider.\n */\ntype ILoggerProvider = LoggerProviderBase & {\n /**\n * Flush all buffered data and shut down the LoggerProvider and all registered\n * LogRecordProcessor.\n *\n * Returns a promise which is resolved when all flushes are complete.\n */\n shutdown?: () => Promise<void>;\n};\n\n/**\n * The way to render the object in the log record. If `\"json\"`,\n * the object is rendered as a JSON string. If `\"inspect\"`,\n * the object is rendered using `util.inspect` in Node.js/Bun, or\n * `Deno.inspect` in Deno.\n */\nexport type ObjectRenderer = \"json\" | \"inspect\";\n\ntype Message = (string | null | undefined)[];\n\n/**\n * Custom `body` attribute formatter.\n * @since 0.3.0\n */\nexport type BodyFormatter = (message: Message) => AnyValue;\n\n/**\n * The OTLP protocol to use for exporting logs.\n * @since 0.9.0\n */\nexport type OtlpProtocol = \"grpc\" | \"http/protobuf\" | \"http/json\";\n\n/**\n * Base options shared by all OpenTelemetry sink configurations.\n */\ninterface OpenTelemetrySinkOptionsBase {\n /**\n * The way to render the message in the log record. If `\"string\"`,\n * the message is rendered as a single string with the values are\n * interpolated into the message. If `\"array\"`, the message is\n * rendered as an array of strings. `\"string\"` by default.\n *\n * Or even fully customizable with a {@link BodyFormatter} function.\n * @since 0.2.0\n */\n messageType?: \"string\" | \"array\" | BodyFormatter;\n\n /**\n * The way to render the object in the log record. If `\"json\"`,\n * the object is rendered as a JSON string. If `\"inspect\"`,\n * the object is rendered using `util.inspect` in Node.js/Bun, or\n * `Deno.inspect` in Deno. `\"inspect\"` by default.\n */\n objectRenderer?: ObjectRenderer;\n\n /**\n * Whether to log diagnostics. Diagnostic logs are logged to\n * the `[\"logtape\", \"meta\", \"otel\"]` category.\n * Turned off by default.\n */\n diagnostics?: boolean;\n}\n\n/**\n * Options for creating an OpenTelemetry sink with a custom logger provider.\n * When using this configuration, you are responsible for setting up the\n * logger provider with appropriate exporters and processors.\n *\n * This is the recommended approach for production use as it gives you\n * full control over the OpenTelemetry configuration.\n * @since 0.9.0\n */\nexport interface OpenTelemetrySinkProviderOptions\n extends OpenTelemetrySinkOptionsBase {\n /**\n * The OpenTelemetry logger provider to use.\n */\n loggerProvider: ILoggerProvider;\n}\n\n/**\n * Options for creating an OpenTelemetry sink with automatic exporter creation.\n * The protocol is determined by environment variables:\n * - `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` (highest priority)\n * - `OTEL_EXPORTER_OTLP_PROTOCOL` (fallback)\n * - Default: `\"http/json\"`\n *\n * For production use, consider providing your own {@link ILoggerProvider}\n * via {@link OpenTelemetrySinkProviderOptions} for more control.\n * @since 0.9.0\n */\nexport interface OpenTelemetrySinkExporterOptions\n extends OpenTelemetrySinkOptionsBase {\n /**\n * The OpenTelemetry logger provider to use.\n * Must be undefined or omitted when using exporter options.\n */\n loggerProvider?: undefined;\n\n /**\n * The OpenTelemetry OTLP exporter configuration to use.\n */\n otlpExporterConfig?: OTLPExporterNodeConfigBase;\n\n /**\n * The service name to use. If not provided, the service name is\n * taken from the `OTEL_SERVICE_NAME` environment variable.\n */\n serviceName?: string;\n}\n\n/**\n * Options for creating an OpenTelemetry sink.\n *\n * This is a union type that accepts either:\n * - {@link OpenTelemetrySinkProviderOptions}: Provide your own `loggerProvider`\n * (recommended for production)\n * - {@link OpenTelemetrySinkExporterOptions}: Let the sink create an exporter\n * automatically based on environment variables\n *\n * When no `loggerProvider` is provided, the protocol is determined by:\n * 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` environment variable\n * 2. `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable\n * 3. Default: `\"http/json\"`\n *\n * @example Using a custom logger provider (recommended)\n * ```typescript\n * import { LoggerProvider, SimpleLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\n * import { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-grpc\";\n *\n * const provider = new LoggerProvider();\n * provider.addLogRecordProcessor(new SimpleLogRecordProcessor(new OTLPLogExporter()));\n *\n * const sink = getOpenTelemetrySink({ loggerProvider: provider });\n * ```\n *\n * @example Using automatic exporter creation\n * ```typescript\n * // Protocol determined by OTEL_EXPORTER_OTLP_PROTOCOL env var\n * const sink = getOpenTelemetrySink({\n * serviceName: \"my-service\",\n * });\n * ```\n */\nexport type OpenTelemetrySinkOptions =\n | OpenTelemetrySinkProviderOptions\n | OpenTelemetrySinkExporterOptions;\n\n/**\n * Initializes the logger provider asynchronously.\n * This is used when the user doesn't provide a custom logger provider.\n * @param options The exporter options.\n * @returns A promise that resolves to the initialized logger provider.\n */\nasync function initializeLoggerProvider(\n options: OpenTelemetrySinkExporterOptions,\n): Promise<ILoggerProvider> {\n const resource = defaultResource().merge(\n resourceFromAttributes({\n [ATTR_SERVICE_NAME]: options.serviceName ??\n getEnvironmentVariable(\"OTEL_SERVICE_NAME\"),\n }),\n );\n const otlpExporter = await createOtlpExporter(options.otlpExporterConfig);\n const loggerProvider = new LoggerProvider({\n resource,\n processors: [\n // @ts-ignore: it works anyway...\n new SimpleLogRecordProcessor(otlpExporter),\n ],\n });\n return loggerProvider;\n}\n\n/**\n * Emits a log record to the OpenTelemetry logger.\n * @param logger The OpenTelemetry logger.\n * @param record The LogTape log record.\n * @param options The sink options.\n */\nfunction emitLogRecord(\n logger: OTLogger,\n record: LogRecord,\n options: OpenTelemetrySinkOptions,\n): void {\n const objectRenderer = options.objectRenderer ?? \"inspect\";\n const { category, level, message, timestamp, properties } = record;\n const severityNumber = mapLevelToSeverityNumber(level);\n const attributes = convertToAttributes(properties, objectRenderer);\n attributes[\"category\"] = [...category];\n logger.emit(\n {\n severityNumber,\n severityText: level,\n body: typeof options.messageType === \"function\"\n ? convertMessageToCustomBodyFormat(\n message,\n objectRenderer,\n options.messageType,\n )\n : options.messageType === \"array\"\n ? convertMessageToArray(message, objectRenderer)\n : convertMessageToString(message, objectRenderer),\n attributes,\n timestamp: new Date(timestamp),\n } satisfies OTLogRecord,\n );\n}\n\n/**\n * Creates a sink that forwards log records to OpenTelemetry.\n *\n * When a custom `loggerProvider` is provided, it is used directly.\n * Otherwise, the sink will lazily initialize a logger provider on the first\n * log record, using the protocol determined by environment variables.\n *\n * @param options Options for creating the sink.\n * @returns The sink.\n */\nexport function getOpenTelemetrySink(\n options: OpenTelemetrySinkOptions = {},\n): Sink & AsyncDisposable {\n if (options.diagnostics) {\n diag.setLogger(new DiagLoggerAdaptor(), DiagLogLevel.DEBUG);\n }\n\n // If loggerProvider is provided, use the synchronous path\n if (options.loggerProvider != null) {\n const loggerProvider = options.loggerProvider;\n const logger = loggerProvider.getLogger(metadata.name, metadata.version);\n const shutdown = loggerProvider.shutdown?.bind(loggerProvider);\n const sink: Sink & AsyncDisposable = Object.assign(\n (record: LogRecord) => {\n const { category } = record;\n if (\n category[0] === \"logtape\" && category[1] === \"meta\" &&\n category[2] === \"otel\"\n ) {\n return;\n }\n emitLogRecord(logger, record, options);\n },\n {\n async [Symbol.asyncDispose](): Promise<void> {\n if (shutdown != null) await shutdown();\n },\n },\n );\n return sink;\n }\n\n // Lazy initialization for automatic exporter creation\n let loggerProvider: ILoggerProvider | null = null;\n let logger: OTLogger | null = null;\n let initPromise: Promise<void> | null = null;\n let initError: Error | null = null;\n\n const sink: Sink & AsyncDisposable = Object.assign(\n (record: LogRecord) => {\n const { category } = record;\n if (\n category[0] === \"logtape\" && category[1] === \"meta\" &&\n category[2] === \"otel\"\n ) {\n return;\n }\n\n // If already initialized, emit the log\n if (logger != null) {\n emitLogRecord(logger, record, options);\n return;\n }\n\n // If initialization failed, skip silently\n if (initError != null) {\n return;\n }\n\n // Start initialization if not already started\n if (initPromise == null) {\n initPromise = initializeLoggerProvider(options)\n .then((provider) => {\n loggerProvider = provider;\n logger = provider.getLogger(metadata.name, metadata.version);\n // Emit the current record that triggered initialization\n emitLogRecord(logger, record, options);\n })\n .catch((error) => {\n initError = error;\n // Log initialization error to console as a fallback\n // deno-lint-ignore no-console\n console.error(\"Failed to initialize OpenTelemetry logger:\", error);\n });\n }\n // Records during initialization are dropped\n // (the triggering record is emitted in the then() callback above)\n },\n {\n async [Symbol.asyncDispose](): Promise<void> {\n // Wait for initialization to complete if in progress\n if (initPromise != null) {\n try {\n await initPromise;\n } catch {\n // Initialization failed, nothing to shut down\n return;\n }\n }\n if (loggerProvider?.shutdown != null) {\n await loggerProvider.shutdown();\n }\n },\n },\n );\n\n return sink;\n}\n\nfunction mapLevelToSeverityNumber(level: string): number {\n switch (level) {\n case \"trace\":\n return SeverityNumber.TRACE;\n case \"debug\":\n return SeverityNumber.DEBUG;\n case \"info\":\n return SeverityNumber.INFO;\n case \"warning\":\n return SeverityNumber.WARN;\n case \"error\":\n return SeverityNumber.ERROR;\n case \"fatal\":\n return SeverityNumber.FATAL;\n default:\n return SeverityNumber.UNSPECIFIED;\n }\n}\n\nfunction convertToAttributes(\n properties: Record<string, unknown>,\n objectRenderer: ObjectRenderer,\n): Record<string, AnyValue> {\n const attributes: Record<string, AnyValue> = {};\n for (const [name, value] of Object.entries(properties)) {\n const key = `attributes.${name}`;\n if (value == null) continue;\n if (Array.isArray(value)) {\n let t = null;\n for (const v of value) {\n if (v == null) continue;\n if (t != null && typeof v !== t) {\n attributes[key] = value.map((v) =>\n convertToString(v, objectRenderer)\n );\n break;\n }\n t = typeof v;\n }\n attributes[key] = value;\n } else {\n const encoded = convertToString(value, objectRenderer);\n if (encoded == null) continue;\n attributes[key] = encoded;\n }\n }\n return attributes;\n}\n\nfunction convertToString(\n value: unknown,\n objectRenderer: ObjectRenderer,\n): string | null | undefined {\n if (value === null || value === undefined || typeof value === \"string\") {\n return value;\n }\n if (objectRenderer === \"inspect\") return inspect(value);\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return value.toString();\n } else if (value instanceof Date) return value.toISOString();\n else return JSON.stringify(value);\n}\n\nfunction convertMessageToArray(\n message: readonly unknown[],\n objectRenderer: ObjectRenderer,\n): AnyValue {\n const body: (string | null | undefined)[] = [];\n for (let i = 0; i < message.length; i += 2) {\n const msg = message[i] as string;\n body.push(msg);\n if (message.length <= i + 1) break;\n const val = message[i + 1];\n body.push(convertToString(val, objectRenderer));\n }\n return body;\n}\n\nfunction convertMessageToString(\n message: readonly unknown[],\n objectRenderer: ObjectRenderer,\n): AnyValue {\n let body = \"\";\n for (let i = 0; i < message.length; i += 2) {\n const msg = message[i] as string;\n body += msg;\n if (message.length <= i + 1) break;\n const val = message[i + 1];\n const extra = convertToString(val, objectRenderer);\n body += extra ?? JSON.stringify(extra);\n }\n return body;\n}\n\nfunction convertMessageToCustomBodyFormat(\n message: readonly unknown[],\n objectRenderer: ObjectRenderer,\n bodyFormatter: BodyFormatter,\n): AnyValue {\n const body = message.map((msg) => convertToString(msg, objectRenderer));\n return bodyFormatter(body);\n}\n\n/**\n * A platform-specific inspect function. In Deno, this is {@link Deno.inspect},\n * and in Node.js/Bun it is {@link util.inspect}. If neither is available, it\n * falls back to {@link JSON.stringify}.\n *\n * @param value The value to inspect.\n * @returns The string representation of the value.\n */\nconst inspect: (value: unknown) => string =\n // @ts-ignore: Deno global\n \"Deno\" in globalThis && \"inspect\" in globalThis.Deno &&\n // @ts-ignore: Deno global\n typeof globalThis.Deno.inspect === \"function\"\n // @ts-ignore: Deno global\n ? globalThis.Deno.inspect\n // @ts-ignore: Node.js global\n : \"util\" in globalThis && \"inspect\" in globalThis.util &&\n // @ts-ignore: Node.js global\n globalThis.util.inspect === \"function\"\n // @ts-ignore: Node.js global\n ? globalThis.util.inspect\n : JSON.stringify;\n\nclass DiagLoggerAdaptor implements DiagLogger {\n logger: Logger;\n\n constructor() {\n this.logger = getLogger([\"logtape\", \"meta\", \"otel\"]);\n }\n\n #escape(msg: string): string {\n return msg.replaceAll(\"{\", \"{{\").replaceAll(\"}\", \"}}\");\n }\n\n error(msg: string, ...values: unknown[]): void {\n this.logger.error(`${this.#escape(msg)}: {values}`, { values });\n }\n\n warn(msg: string, ...values: unknown[]): void {\n this.logger.warn(`${this.#escape(msg)}: {values}`, { values });\n }\n\n info(msg: string, ...values: unknown[]): void {\n this.logger.info(`${this.#escape(msg)}: {values}`, { values });\n }\n\n debug(msg: string, ...values: unknown[]): void {\n this.logger.debug(`${this.#escape(msg)}: {values}`, { values });\n }\n\n verbose(msg: string, ...values: unknown[]): void {\n this.logger.debug(`${this.#escape(msg)}: {values}`, { values });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mod.js","names":["name: string","config?: OTLPExporterNodeConfigBase","noopLoggerProvider: ILoggerProvider","options: OpenTelemetrySinkExporterOptions","LoggerProvider","logger: OTLogger","record: LogRecord","options: OpenTelemetrySinkOptions","loggerProvider","logger","metadata","sink: Sink & AsyncDisposable","sink","loggerProvider: ILoggerProvider | null","logger: OTLogger | null","initPromise: Promise<void> | null","initError: Error | null","level: string","properties: Record<string, unknown>","objectRenderer: ObjectRenderer","attributes: Record<string, AnyValue>","v","value: unknown","message: readonly unknown[]","body: (string | null | undefined)[]","bodyFormatter: BodyFormatter","inspect: (value: unknown) => string","msg: string","#escape"],"sources":["../src/mod.ts"],"sourcesContent":["import {\n getLogger,\n type Logger,\n type LogRecord,\n type Sink,\n} from \"@logtape/logtape\";\nimport { diag, type DiagLogger, DiagLogLevel } from \"@opentelemetry/api\";\nimport {\n type AnyValue,\n type Logger as OTLogger,\n type LoggerProvider as LoggerProviderBase,\n type LogRecord as OTLogRecord,\n NOOP_LOGGER,\n SeverityNumber,\n} from \"@opentelemetry/api-logs\";\nimport type { OTLPExporterNodeConfigBase } from \"@opentelemetry/otlp-exporter-base\";\nimport {\n defaultResource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport {\n LoggerProvider,\n SimpleLogRecordProcessor,\n} from \"@opentelemetry/sdk-logs\";\nimport { ATTR_SERVICE_NAME } from \"@opentelemetry/semantic-conventions\";\nimport metadata from \"../deno.json\" with { type: \"json\" };\n\n/**\n * Gets an environment variable value across different JavaScript runtimes.\n * @param name The environment variable name.\n * @returns The environment variable value, or undefined if not found.\n */\nfunction getEnvironmentVariable(name: string): string | undefined {\n // Deno runtime\n if (typeof Deno !== \"undefined\" && Deno.env) {\n try {\n return Deno.env.get(name);\n } catch {\n // Deno.env.get() can throw if permissions are not granted\n return undefined;\n }\n }\n\n // Node.js/Bun runtime\n if (\n typeof globalThis !== \"undefined\" && \"process\" in globalThis &&\n // @ts-ignore: process exists in Node.js/Bun\n typeof globalThis.process !== \"undefined\" &&\n // @ts-ignore: process.env exists in Node.js/Bun\n typeof globalThis.process.env === \"object\" &&\n // @ts-ignore: process.env exists in Node.js/Bun\n globalThis.process.env !== null\n ) {\n // @ts-ignore: process.env exists in Node.js/Bun\n return globalThis.process.env[name];\n }\n\n // Browser/other environments - no environment variables available\n return undefined;\n}\n\n/**\n * Checks if an OTLP endpoint is configured via environment variables or options.\n * Checks the following environment variables:\n * - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (logs-specific endpoint)\n * - `OTEL_EXPORTER_OTLP_ENDPOINT` (general OTLP endpoint)\n *\n * @param config Optional exporter configuration that may contain a URL.\n * @returns `true` if an endpoint is configured, `false` otherwise.\n */\nfunction hasOtlpEndpoint(config?: OTLPExporterNodeConfigBase): boolean {\n // Check if URL is provided in config\n if (config?.url) {\n return true;\n }\n\n // Check environment variables\n const logsEndpoint = getEnvironmentVariable(\n \"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT\",\n );\n if (logsEndpoint) {\n return true;\n }\n\n const endpoint = getEnvironmentVariable(\"OTEL_EXPORTER_OTLP_ENDPOINT\");\n if (endpoint) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Detects the OTLP protocol from environment variables.\n * Priority:\n * 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`\n * 2. `OTEL_EXPORTER_OTLP_PROTOCOL`\n * 3. Default: `\"http/json\"` (for backward compatibility)\n *\n * @returns The detected OTLP protocol.\n */\nfunction detectOtlpProtocol(): OtlpProtocol {\n const logsProtocol = getEnvironmentVariable(\n \"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL\",\n );\n if (\n logsProtocol === \"grpc\" ||\n logsProtocol === \"http/protobuf\" ||\n logsProtocol === \"http/json\"\n ) {\n return logsProtocol;\n }\n\n const protocol = getEnvironmentVariable(\"OTEL_EXPORTER_OTLP_PROTOCOL\");\n if (\n protocol === \"grpc\" ||\n protocol === \"http/protobuf\" ||\n protocol === \"http/json\"\n ) {\n return protocol;\n }\n\n return \"http/json\";\n}\n\n/**\n * Creates an OTLP log exporter based on the detected protocol.\n * Uses dynamic imports to maintain browser compatibility when gRPC is not used.\n * @param config Optional exporter configuration.\n * @returns A promise that resolves to the appropriate OTLP log exporter.\n */\nasync function createOtlpExporter(\n config?: OTLPExporterNodeConfigBase,\n // deno-lint-ignore no-explicit-any\n): Promise<any> {\n const protocol = detectOtlpProtocol();\n\n switch (protocol) {\n case \"grpc\": {\n const { OTLPLogExporter } = await import(\n \"@opentelemetry/exporter-logs-otlp-grpc\"\n );\n return new OTLPLogExporter(config);\n }\n case \"http/protobuf\": {\n const { OTLPLogExporter } = await import(\n \"@opentelemetry/exporter-logs-otlp-proto\"\n );\n return new OTLPLogExporter(config);\n }\n case \"http/json\":\n default: {\n const { OTLPLogExporter } = await import(\n \"@opentelemetry/exporter-logs-otlp-http\"\n );\n return new OTLPLogExporter(config);\n }\n }\n}\n\n/**\n * The OpenTelemetry logger provider.\n */\ntype ILoggerProvider = LoggerProviderBase & {\n /**\n * Flush all buffered data and shut down the LoggerProvider and all registered\n * LogRecordProcessor.\n *\n * Returns a promise which is resolved when all flushes are complete.\n */\n shutdown?: () => Promise<void>;\n};\n\n/**\n * The way to render the object in the log record. If `\"json\"`,\n * the object is rendered as a JSON string. If `\"inspect\"`,\n * the object is rendered using `util.inspect` in Node.js/Bun, or\n * `Deno.inspect` in Deno.\n */\nexport type ObjectRenderer = \"json\" | \"inspect\";\n\ntype Message = (string | null | undefined)[];\n\n/**\n * Custom `body` attribute formatter.\n * @since 0.3.0\n */\nexport type BodyFormatter = (message: Message) => AnyValue;\n\n/**\n * The OTLP protocol to use for exporting logs.\n * @since 0.9.0\n */\nexport type OtlpProtocol = \"grpc\" | \"http/protobuf\" | \"http/json\";\n\n/**\n * Base options shared by all OpenTelemetry sink configurations.\n */\ninterface OpenTelemetrySinkOptionsBase {\n /**\n * The way to render the message in the log record. If `\"string\"`,\n * the message is rendered as a single string with the values are\n * interpolated into the message. If `\"array\"`, the message is\n * rendered as an array of strings. `\"string\"` by default.\n *\n * Or even fully customizable with a {@link BodyFormatter} function.\n * @since 0.2.0\n */\n messageType?: \"string\" | \"array\" | BodyFormatter;\n\n /**\n * The way to render the object in the log record. If `\"json\"`,\n * the object is rendered as a JSON string. If `\"inspect\"`,\n * the object is rendered using `util.inspect` in Node.js/Bun, or\n * `Deno.inspect` in Deno. `\"inspect\"` by default.\n */\n objectRenderer?: ObjectRenderer;\n\n /**\n * Whether to log diagnostics. Diagnostic logs are logged to\n * the `[\"logtape\", \"meta\", \"otel\"]` category.\n * Turned off by default.\n */\n diagnostics?: boolean;\n}\n\n/**\n * Options for creating an OpenTelemetry sink with a custom logger provider.\n * When using this configuration, you are responsible for setting up the\n * logger provider with appropriate exporters and processors.\n *\n * This is the recommended approach for production use as it gives you\n * full control over the OpenTelemetry configuration.\n * @since 0.9.0\n */\nexport interface OpenTelemetrySinkProviderOptions\n extends OpenTelemetrySinkOptionsBase {\n /**\n * The OpenTelemetry logger provider to use.\n */\n loggerProvider: ILoggerProvider;\n}\n\n/**\n * Options for creating an OpenTelemetry sink with automatic exporter creation.\n * The protocol is determined by environment variables:\n * - `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` (highest priority)\n * - `OTEL_EXPORTER_OTLP_PROTOCOL` (fallback)\n * - Default: `\"http/json\"`\n *\n * For production use, consider providing your own {@link ILoggerProvider}\n * via {@link OpenTelemetrySinkProviderOptions} for more control.\n * @since 0.9.0\n */\nexport interface OpenTelemetrySinkExporterOptions\n extends OpenTelemetrySinkOptionsBase {\n /**\n * The OpenTelemetry logger provider to use.\n * Must be undefined or omitted when using exporter options.\n */\n loggerProvider?: undefined;\n\n /**\n * The OpenTelemetry OTLP exporter configuration to use.\n */\n otlpExporterConfig?: OTLPExporterNodeConfigBase;\n\n /**\n * The service name to use. If not provided, the service name is\n * taken from the `OTEL_SERVICE_NAME` environment variable.\n */\n serviceName?: string;\n}\n\n/**\n * Options for creating an OpenTelemetry sink.\n *\n * This is a union type that accepts either:\n * - {@link OpenTelemetrySinkProviderOptions}: Provide your own `loggerProvider`\n * (recommended for production)\n * - {@link OpenTelemetrySinkExporterOptions}: Let the sink create an exporter\n * automatically based on environment variables\n *\n * When no `loggerProvider` is provided, the protocol is determined by:\n * 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` environment variable\n * 2. `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable\n * 3. Default: `\"http/json\"`\n *\n * @example Using a custom logger provider (recommended)\n * ```typescript\n * import { LoggerProvider, SimpleLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\n * import { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-grpc\";\n *\n * const provider = new LoggerProvider();\n * provider.addLogRecordProcessor(new SimpleLogRecordProcessor(new OTLPLogExporter()));\n *\n * const sink = getOpenTelemetrySink({ loggerProvider: provider });\n * ```\n *\n * @example Using automatic exporter creation\n * ```typescript\n * // Protocol determined by OTEL_EXPORTER_OTLP_PROTOCOL env var\n * const sink = getOpenTelemetrySink({\n * serviceName: \"my-service\",\n * });\n * ```\n */\nexport type OpenTelemetrySinkOptions =\n | OpenTelemetrySinkProviderOptions\n | OpenTelemetrySinkExporterOptions;\n\n/**\n * A no-op logger provider that returns NOOP_LOGGER for all requests.\n * Used when no OTLP endpoint is configured to avoid repeated connection errors.\n */\nconst noopLoggerProvider: ILoggerProvider = {\n getLogger: () => NOOP_LOGGER,\n};\n\n/**\n * Initializes the logger provider asynchronously.\n * This is used when the user doesn't provide a custom logger provider.\n *\n * If no OTLP endpoint is configured (via options or environment variables),\n * returns a noop logger provider to avoid repeated connection errors.\n *\n * @param options The exporter options.\n * @returns A promise that resolves to the initialized logger provider.\n */\nasync function initializeLoggerProvider(\n options: OpenTelemetrySinkExporterOptions,\n): Promise<ILoggerProvider> {\n // If no endpoint is configured, use noop logger provider to avoid\n // repeated transport errors\n if (!hasOtlpEndpoint(options.otlpExporterConfig)) {\n return noopLoggerProvider;\n }\n\n const resource = defaultResource().merge(\n resourceFromAttributes({\n [ATTR_SERVICE_NAME]: options.serviceName ??\n getEnvironmentVariable(\"OTEL_SERVICE_NAME\"),\n }),\n );\n const otlpExporter = await createOtlpExporter(options.otlpExporterConfig);\n const loggerProvider = new LoggerProvider({\n resource,\n processors: [\n // @ts-ignore: it works anyway...\n new SimpleLogRecordProcessor(otlpExporter),\n ],\n });\n return loggerProvider;\n}\n\n/**\n * Emits a log record to the OpenTelemetry logger.\n * @param logger The OpenTelemetry logger.\n * @param record The LogTape log record.\n * @param options The sink options.\n */\nfunction emitLogRecord(\n logger: OTLogger,\n record: LogRecord,\n options: OpenTelemetrySinkOptions,\n): void {\n const objectRenderer = options.objectRenderer ?? \"inspect\";\n const { category, level, message, timestamp, properties } = record;\n const severityNumber = mapLevelToSeverityNumber(level);\n const attributes = convertToAttributes(properties, objectRenderer);\n attributes[\"category\"] = [...category];\n logger.emit(\n {\n severityNumber,\n severityText: level,\n body: typeof options.messageType === \"function\"\n ? convertMessageToCustomBodyFormat(\n message,\n objectRenderer,\n options.messageType,\n )\n : options.messageType === \"array\"\n ? convertMessageToArray(message, objectRenderer)\n : convertMessageToString(message, objectRenderer),\n attributes,\n timestamp: new Date(timestamp),\n } satisfies OTLogRecord,\n );\n}\n\n/**\n * Creates a sink that forwards log records to OpenTelemetry.\n *\n * When a custom `loggerProvider` is provided, it is used directly.\n * Otherwise, the sink will lazily initialize a logger provider on the first\n * log record, using the protocol determined by environment variables.\n *\n * @param options Options for creating the sink.\n * @returns The sink.\n */\nexport function getOpenTelemetrySink(\n options: OpenTelemetrySinkOptions = {},\n): Sink & AsyncDisposable {\n if (options.diagnostics) {\n diag.setLogger(new DiagLoggerAdaptor(), DiagLogLevel.DEBUG);\n }\n\n // If loggerProvider is provided, use the synchronous path\n if (options.loggerProvider != null) {\n const loggerProvider = options.loggerProvider;\n const logger = loggerProvider.getLogger(metadata.name, metadata.version);\n const shutdown = loggerProvider.shutdown?.bind(loggerProvider);\n const sink: Sink & AsyncDisposable = Object.assign(\n (record: LogRecord) => {\n const { category } = record;\n if (\n category[0] === \"logtape\" && category[1] === \"meta\" &&\n category[2] === \"otel\"\n ) {\n return;\n }\n emitLogRecord(logger, record, options);\n },\n {\n async [Symbol.asyncDispose](): Promise<void> {\n if (shutdown != null) await shutdown();\n },\n },\n );\n return sink;\n }\n\n // Lazy initialization for automatic exporter creation\n let loggerProvider: ILoggerProvider | null = null;\n let logger: OTLogger | null = null;\n let initPromise: Promise<void> | null = null;\n let initError: Error | null = null;\n\n const sink: Sink & AsyncDisposable = Object.assign(\n (record: LogRecord) => {\n const { category } = record;\n if (\n category[0] === \"logtape\" && category[1] === \"meta\" &&\n category[2] === \"otel\"\n ) {\n return;\n }\n\n // If already initialized, emit the log\n if (logger != null) {\n emitLogRecord(logger, record, options);\n return;\n }\n\n // If initialization failed, skip silently\n if (initError != null) {\n return;\n }\n\n // Start initialization if not already started\n if (initPromise == null) {\n initPromise = initializeLoggerProvider(options)\n .then((provider) => {\n loggerProvider = provider;\n logger = provider.getLogger(metadata.name, metadata.version);\n // Emit the current record that triggered initialization\n emitLogRecord(logger, record, options);\n })\n .catch((error) => {\n initError = error;\n // Log initialization error to console as a fallback\n // deno-lint-ignore no-console\n console.error(\"Failed to initialize OpenTelemetry logger:\", error);\n });\n }\n // Records during initialization are dropped\n // (the triggering record is emitted in the then() callback above)\n },\n {\n async [Symbol.asyncDispose](): Promise<void> {\n // Wait for initialization to complete if in progress\n if (initPromise != null) {\n try {\n await initPromise;\n } catch {\n // Initialization failed, nothing to shut down\n return;\n }\n }\n if (loggerProvider?.shutdown != null) {\n await loggerProvider.shutdown();\n }\n },\n },\n );\n\n return sink;\n}\n\nfunction mapLevelToSeverityNumber(level: string): number {\n switch (level) {\n case \"trace\":\n return SeverityNumber.TRACE;\n case \"debug\":\n return SeverityNumber.DEBUG;\n case \"info\":\n return SeverityNumber.INFO;\n case \"warning\":\n return SeverityNumber.WARN;\n case \"error\":\n return SeverityNumber.ERROR;\n case \"fatal\":\n return SeverityNumber.FATAL;\n default:\n return SeverityNumber.UNSPECIFIED;\n }\n}\n\nfunction convertToAttributes(\n properties: Record<string, unknown>,\n objectRenderer: ObjectRenderer,\n): Record<string, AnyValue> {\n const attributes: Record<string, AnyValue> = {};\n for (const [name, value] of Object.entries(properties)) {\n const key = `attributes.${name}`;\n if (value == null) continue;\n if (Array.isArray(value)) {\n let t = null;\n for (const v of value) {\n if (v == null) continue;\n if (t != null && typeof v !== t) {\n attributes[key] = value.map((v) =>\n convertToString(v, objectRenderer)\n );\n break;\n }\n t = typeof v;\n }\n attributes[key] = value;\n } else {\n const encoded = convertToString(value, objectRenderer);\n if (encoded == null) continue;\n attributes[key] = encoded;\n }\n }\n return attributes;\n}\n\nfunction convertToString(\n value: unknown,\n objectRenderer: ObjectRenderer,\n): string | null | undefined {\n if (value === null || value === undefined || typeof value === \"string\") {\n return value;\n }\n if (objectRenderer === \"inspect\") return inspect(value);\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return value.toString();\n } else if (value instanceof Date) return value.toISOString();\n else return JSON.stringify(value);\n}\n\nfunction convertMessageToArray(\n message: readonly unknown[],\n objectRenderer: ObjectRenderer,\n): AnyValue {\n const body: (string | null | undefined)[] = [];\n for (let i = 0; i < message.length; i += 2) {\n const msg = message[i] as string;\n body.push(msg);\n if (message.length <= i + 1) break;\n const val = message[i + 1];\n body.push(convertToString(val, objectRenderer));\n }\n return body;\n}\n\nfunction convertMessageToString(\n message: readonly unknown[],\n objectRenderer: ObjectRenderer,\n): AnyValue {\n let body = \"\";\n for (let i = 0; i < message.length; i += 2) {\n const msg = message[i] as string;\n body += msg;\n if (message.length <= i + 1) break;\n const val = message[i + 1];\n const extra = convertToString(val, objectRenderer);\n body += extra ?? JSON.stringify(extra);\n }\n return body;\n}\n\nfunction convertMessageToCustomBodyFormat(\n message: readonly unknown[],\n objectRenderer: ObjectRenderer,\n bodyFormatter: BodyFormatter,\n): AnyValue {\n const body = message.map((msg) => convertToString(msg, objectRenderer));\n return bodyFormatter(body);\n}\n\n/**\n * A platform-specific inspect function. In Deno, this is {@link Deno.inspect},\n * and in Node.js/Bun it is {@link util.inspect}. If neither is available, it\n * falls back to {@link JSON.stringify}.\n *\n * @param value The value to inspect.\n * @returns The string representation of the value.\n */\nconst inspect: (value: unknown) => string =\n // @ts-ignore: Deno global\n \"Deno\" in globalThis && \"inspect\" in globalThis.Deno &&\n // @ts-ignore: Deno global\n typeof globalThis.Deno.inspect === \"function\"\n // @ts-ignore: Deno global\n ? globalThis.Deno.inspect\n // @ts-ignore: Node.js global\n : \"util\" in globalThis && \"inspect\" in globalThis.util &&\n // @ts-ignore: Node.js global\n globalThis.util.inspect === \"function\"\n // @ts-ignore: Node.js global\n ? globalThis.util.inspect\n : JSON.stringify;\n\nclass DiagLoggerAdaptor implements DiagLogger {\n logger: Logger;\n\n constructor() {\n this.logger = getLogger([\"logtape\", \"meta\", \"otel\"]);\n }\n\n #escape(msg: string): string {\n return msg.replaceAll(\"{\", \"{{\").replaceAll(\"}\", \"}}\");\n }\n\n error(msg: string, ...values: unknown[]): void {\n this.logger.error(`${this.#escape(msg)}: {values}`, { values });\n }\n\n warn(msg: string, ...values: unknown[]): void {\n this.logger.warn(`${this.#escape(msg)}: {values}`, { values });\n }\n\n info(msg: string, ...values: unknown[]): void {\n this.logger.info(`${this.#escape(msg)}: {values}`, { values });\n }\n\n debug(msg: string, ...values: unknown[]): void {\n this.logger.debug(`${this.#escape(msg)}: {values}`, { values });\n }\n\n verbose(msg: string, ...values: unknown[]): void {\n this.logger.debug(`${this.#escape(msg)}: {values}`, { values });\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/otel",
|
|
3
|
-
"version": "1.3.0-dev.
|
|
3
|
+
"version": "1.3.0-dev.382+d8530ecb",
|
|
4
4
|
"description": "LogTape OpenTelemetry sink",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"LogTape",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"sideEffects": false,
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@logtape/logtape": "^1.3.0-dev.
|
|
45
|
+
"@logtape/logtape": "^1.3.0-dev.382+d8530ecb"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@opentelemetry/api": "^1.9.0",
|
package/src/mod.test.ts
CHANGED
|
@@ -638,3 +638,57 @@ test("sink handles rapid succession of logs", () => {
|
|
|
638
638
|
|
|
639
639
|
assertEquals(emittedRecords.length, count);
|
|
640
640
|
});
|
|
641
|
+
|
|
642
|
+
// =============================================================================
|
|
643
|
+
// NoopLogger fallback tests (when no endpoint is configured)
|
|
644
|
+
// =============================================================================
|
|
645
|
+
|
|
646
|
+
test("sink with no endpoint config creates valid sink function", () => {
|
|
647
|
+
// When no endpoint is configured (no env vars, no url in config),
|
|
648
|
+
// the sink should still work without throwing errors
|
|
649
|
+
const sink = getOpenTelemetrySink({
|
|
650
|
+
serviceName: "test-service",
|
|
651
|
+
// No otlpExporterConfig.url and no OTEL_EXPORTER_OTLP_ENDPOINT env var
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
assertEquals(typeof sink, "function");
|
|
655
|
+
assertEquals(typeof sink[Symbol.asyncDispose], "function");
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("sink with no endpoint accepts logs without errors", () => {
|
|
659
|
+
const sink = getOpenTelemetrySink({
|
|
660
|
+
serviceName: "test-service",
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Should not throw when logging without an endpoint
|
|
664
|
+
const record = createMockLogRecord();
|
|
665
|
+
sink(record);
|
|
666
|
+
sink(record);
|
|
667
|
+
sink(record);
|
|
668
|
+
|
|
669
|
+
// No assertion needed - we're just verifying it doesn't throw
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("sink with explicit url in config should not use noop", () => {
|
|
673
|
+
// When a URL is explicitly provided, it should attempt to use a real exporter
|
|
674
|
+
const sink = getOpenTelemetrySink({
|
|
675
|
+
serviceName: "test-service",
|
|
676
|
+
otlpExporterConfig: {
|
|
677
|
+
url: "http://localhost:4318/v1/logs",
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
assertEquals(typeof sink, "function");
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("sink with no endpoint can be disposed cleanly", async () => {
|
|
685
|
+
const sink = getOpenTelemetrySink({
|
|
686
|
+
serviceName: "test-service",
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Log something to trigger lazy initialization
|
|
690
|
+
sink(createMockLogRecord());
|
|
691
|
+
|
|
692
|
+
// Should not throw when disposing
|
|
693
|
+
await sink[Symbol.asyncDispose]();
|
|
694
|
+
});
|
package/src/mod.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type Logger as OTLogger,
|
|
11
11
|
type LoggerProvider as LoggerProviderBase,
|
|
12
12
|
type LogRecord as OTLogRecord,
|
|
13
|
+
NOOP_LOGGER,
|
|
13
14
|
SeverityNumber,
|
|
14
15
|
} from "@opentelemetry/api-logs";
|
|
15
16
|
import type { OTLPExporterNodeConfigBase } from "@opentelemetry/otlp-exporter-base";
|
|
@@ -58,6 +59,37 @@ function getEnvironmentVariable(name: string): string | undefined {
|
|
|
58
59
|
return undefined;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Checks if an OTLP endpoint is configured via environment variables or options.
|
|
64
|
+
* Checks the following environment variables:
|
|
65
|
+
* - `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` (logs-specific endpoint)
|
|
66
|
+
* - `OTEL_EXPORTER_OTLP_ENDPOINT` (general OTLP endpoint)
|
|
67
|
+
*
|
|
68
|
+
* @param config Optional exporter configuration that may contain a URL.
|
|
69
|
+
* @returns `true` if an endpoint is configured, `false` otherwise.
|
|
70
|
+
*/
|
|
71
|
+
function hasOtlpEndpoint(config?: OTLPExporterNodeConfigBase): boolean {
|
|
72
|
+
// Check if URL is provided in config
|
|
73
|
+
if (config?.url) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check environment variables
|
|
78
|
+
const logsEndpoint = getEnvironmentVariable(
|
|
79
|
+
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
|
80
|
+
);
|
|
81
|
+
if (logsEndpoint) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const endpoint = getEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT");
|
|
86
|
+
if (endpoint) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
61
93
|
/**
|
|
62
94
|
* Detects the OTLP protocol from environment variables.
|
|
63
95
|
* Priority:
|
|
@@ -277,15 +309,33 @@ export type OpenTelemetrySinkOptions =
|
|
|
277
309
|
| OpenTelemetrySinkProviderOptions
|
|
278
310
|
| OpenTelemetrySinkExporterOptions;
|
|
279
311
|
|
|
312
|
+
/**
|
|
313
|
+
* A no-op logger provider that returns NOOP_LOGGER for all requests.
|
|
314
|
+
* Used when no OTLP endpoint is configured to avoid repeated connection errors.
|
|
315
|
+
*/
|
|
316
|
+
const noopLoggerProvider: ILoggerProvider = {
|
|
317
|
+
getLogger: () => NOOP_LOGGER,
|
|
318
|
+
};
|
|
319
|
+
|
|
280
320
|
/**
|
|
281
321
|
* Initializes the logger provider asynchronously.
|
|
282
322
|
* This is used when the user doesn't provide a custom logger provider.
|
|
323
|
+
*
|
|
324
|
+
* If no OTLP endpoint is configured (via options or environment variables),
|
|
325
|
+
* returns a noop logger provider to avoid repeated connection errors.
|
|
326
|
+
*
|
|
283
327
|
* @param options The exporter options.
|
|
284
328
|
* @returns A promise that resolves to the initialized logger provider.
|
|
285
329
|
*/
|
|
286
330
|
async function initializeLoggerProvider(
|
|
287
331
|
options: OpenTelemetrySinkExporterOptions,
|
|
288
332
|
): Promise<ILoggerProvider> {
|
|
333
|
+
// If no endpoint is configured, use noop logger provider to avoid
|
|
334
|
+
// repeated transport errors
|
|
335
|
+
if (!hasOtlpEndpoint(options.otlpExporterConfig)) {
|
|
336
|
+
return noopLoggerProvider;
|
|
337
|
+
}
|
|
338
|
+
|
|
289
339
|
const resource = defaultResource().merge(
|
|
290
340
|
resourceFromAttributes({
|
|
291
341
|
[ATTR_SERVICE_NAME]: options.serviceName ??
|