@iskra-bun/core 0.1.1 → 0.3.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/CHANGELOG.md +74 -0
- package/README.md +3 -2
- package/dist/index.d.ts +690 -108
- package/dist/index.js +526 -62
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- package/src/app.ts +219 -21
- package/src/config/loader.ts +9 -2
- package/src/config/schema.ts +78 -23
- package/src/env.ts +31 -0
- package/src/errors.ts +32 -3
- package/src/index.ts +2 -0
- package/src/logger/index.ts +190 -28
- package/src/otel.ts +221 -25
- package/src/traced.ts +59 -0
- package/src/types.ts +61 -8
package/src/otel.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* actionable error instead of an unhandled module-resolution failure.
|
|
13
13
|
*/
|
|
14
14
|
import type { OtelConfig } from './types';
|
|
15
|
+
import { nodeEnv } from './env';
|
|
15
16
|
|
|
16
|
-
let sdkInstance:
|
|
17
|
+
let sdkInstance: { shutdown(): Promise<void> } | null = null;
|
|
17
18
|
|
|
18
19
|
/** Optional OTel package specifiers, indirected so tsc does not resolve them statically. */
|
|
19
20
|
const OTEL_MODULES = {
|
|
@@ -26,9 +27,202 @@ const OTEL_MODULES = {
|
|
|
26
27
|
semconv: '@opentelemetry/semantic-conventions',
|
|
27
28
|
} as const;
|
|
28
29
|
|
|
30
|
+
// The optional OTel packages are loaded by specifier, untyped; these are the
|
|
31
|
+
// parts of them used here.
|
|
32
|
+
type Ctor<T = unknown> = new (options: Record<string, unknown>) => T;
|
|
33
|
+
interface OtelSdkNode {
|
|
34
|
+
NodeSDK: Ctor<{ start(): void; shutdown(): Promise<void> }>;
|
|
35
|
+
}
|
|
36
|
+
interface OtelAutoInstrumentations {
|
|
37
|
+
getNodeAutoInstrumentations(overrides: Record<string, unknown>): unknown;
|
|
38
|
+
}
|
|
39
|
+
interface OtelTraceExporter {
|
|
40
|
+
OTLPTraceExporter: Ctor;
|
|
41
|
+
}
|
|
42
|
+
interface OtelMetricExporter {
|
|
43
|
+
OTLPMetricExporter: Ctor;
|
|
44
|
+
}
|
|
45
|
+
interface OtelSdkMetrics {
|
|
46
|
+
PeriodicExportingMetricReader: Ctor;
|
|
47
|
+
}
|
|
48
|
+
interface OtelSemconv {
|
|
49
|
+
ATTR_SERVICE_NAME?: string;
|
|
50
|
+
ATTR_SERVICE_VERSION?: string;
|
|
51
|
+
}
|
|
52
|
+
/** `@opentelemetry/resources`: 2.x exports resourceFromAttributes(), 1.x the Resource class. */
|
|
53
|
+
export interface OtelResourcesModule {
|
|
54
|
+
resourceFromAttributes?: (attributes: Record<string, string>) => unknown;
|
|
55
|
+
Resource?: new (attributes: Record<string, string>) => unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
29
58
|
/** Import an optional module by specifier; the indirection keeps tsc from resolving it. */
|
|
30
|
-
function importOptional(specifier: string): Promise<
|
|
31
|
-
return import(specifier)
|
|
59
|
+
function importOptional<T>(specifier: string): Promise<T> {
|
|
60
|
+
return import(specifier) as Promise<T>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_ENDPOINT = 'http://localhost:4318';
|
|
64
|
+
const HTTP_INSTRUMENTATION = '@opentelemetry/instrumentation-http';
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Query parameters whose values are exported as REDACTED in span URLs (see
|
|
68
|
+
* redactUrl). Names are compared without case, `-` or `_`.
|
|
69
|
+
*/
|
|
70
|
+
export const SECRET_QUERY_PARAMS: readonly string[] = [
|
|
71
|
+
'token',
|
|
72
|
+
'access_token',
|
|
73
|
+
'refresh_token',
|
|
74
|
+
'id_token',
|
|
75
|
+
'auth_token',
|
|
76
|
+
'session_token',
|
|
77
|
+
'api_key',
|
|
78
|
+
'key',
|
|
79
|
+
'secret',
|
|
80
|
+
'client_secret',
|
|
81
|
+
'password',
|
|
82
|
+
'passwd',
|
|
83
|
+
'code',
|
|
84
|
+
'state',
|
|
85
|
+
'ticket',
|
|
86
|
+
'otp',
|
|
87
|
+
'jwt',
|
|
88
|
+
'signature',
|
|
89
|
+
'sig',
|
|
90
|
+
'X-Amz-Signature',
|
|
91
|
+
'X-Amz-Credential',
|
|
92
|
+
'X-Amz-Security-Token',
|
|
93
|
+
'AWSAccessKeyId',
|
|
94
|
+
'X-Goog-Signature',
|
|
95
|
+
'X-Goog-Credential',
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const normalizeParam = (name: string): string => name.toLowerCase().replace(/[-_]/g, '');
|
|
99
|
+
|
|
100
|
+
function decodeParam(name: string): string {
|
|
101
|
+
try {
|
|
102
|
+
return decodeURIComponent(name.replace(/\+/g, ' '));
|
|
103
|
+
} catch {
|
|
104
|
+
return name;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `url` (absolute, or a path) with the value of each query parameter named in
|
|
110
|
+
* `params` replaced by REDACTED, the rest kept as it was. Telemetry exported
|
|
111
|
+
* URLs as they were requested: `?api_key=`, `?access_token=`, a presigned
|
|
112
|
+
* URL's signature.
|
|
113
|
+
*/
|
|
114
|
+
export function redactUrl(url: string, params: readonly string[] = SECRET_QUERY_PARAMS): string {
|
|
115
|
+
const start = url.indexOf('?');
|
|
116
|
+
if (start === -1 || params.length === 0) return url;
|
|
117
|
+
const names = new Set(params.map(normalizeParam));
|
|
118
|
+
const hash = url.indexOf('#', start);
|
|
119
|
+
const end = hash === -1 ? url.length : hash;
|
|
120
|
+
let redacted = false;
|
|
121
|
+
const query = url
|
|
122
|
+
.slice(start + 1, end)
|
|
123
|
+
.split('&')
|
|
124
|
+
.map((pair) => {
|
|
125
|
+
const eq = pair.indexOf('=');
|
|
126
|
+
if (eq === -1 || !names.has(normalizeParam(decodeParam(pair.slice(0, eq))))) return pair;
|
|
127
|
+
redacted = true;
|
|
128
|
+
return `${pair.slice(0, eq)}=REDACTED`;
|
|
129
|
+
})
|
|
130
|
+
.join('&');
|
|
131
|
+
return redacted ? url.slice(0, start + 1) + query + url.slice(end) : url;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The URL attributes of an HTTP span, in the stable and the older semantic conventions. */
|
|
135
|
+
const URL_ATTRIBUTES = ['url.full', 'url.query', 'http.url', 'http.target'];
|
|
136
|
+
|
|
137
|
+
/** Rewrites the URL attributes of a recording SDK span (it has `attributes`). */
|
|
138
|
+
function redactSpanUrls(span: unknown, params: readonly string[]): void {
|
|
139
|
+
const { attributes, setAttribute } = (span ?? {}) as {
|
|
140
|
+
attributes?: Record<string, unknown>;
|
|
141
|
+
setAttribute?: (key: string, value: string) => unknown;
|
|
142
|
+
};
|
|
143
|
+
if (!attributes || typeof setAttribute !== 'function') return;
|
|
144
|
+
for (const key of URL_ATTRIBUTES) {
|
|
145
|
+
const value = attributes[key];
|
|
146
|
+
if (typeof value !== 'string') continue;
|
|
147
|
+
// url.query is the query string alone, without its "?".
|
|
148
|
+
const redacted = key === 'url.query' ? redactUrl(`?${value}`, params).slice(1) : redactUrl(value, params);
|
|
149
|
+
if (redacted !== value) setAttribute.call(span, key, redacted);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The options initOtel() passes to getNodeAutoInstrumentations(): the app's
|
|
155
|
+
* `instrumentations` over Iskra's defaults. The HTTP instrumentation redacts
|
|
156
|
+
* SECRET_QUERY_PARAMS unless `redactedQueryParams` / `redactedQueryParamsServer`
|
|
157
|
+
* are set (instrumentation-http 0.204 / 0.222 and later apply them itself;
|
|
158
|
+
* for older releases a requestHook rewrites the span's URL attributes, before
|
|
159
|
+
* the app's own requestHook runs).
|
|
160
|
+
*/
|
|
161
|
+
export function autoInstrumentationOptions(config: OtelConfig): Record<string, unknown> {
|
|
162
|
+
const http: Record<string, unknown> = { ...config.instrumentations?.[HTTP_INSTRUMENTATION] };
|
|
163
|
+
const list = (value: unknown) => (Array.isArray(value) ? (value as string[]) : undefined);
|
|
164
|
+
const client = list(http.redactedQueryParams) ?? SECRET_QUERY_PARAMS;
|
|
165
|
+
const server = list(http.redactedQueryParamsServer) ?? SECRET_QUERY_PARAMS;
|
|
166
|
+
const appHook = http.requestHook;
|
|
167
|
+
return {
|
|
168
|
+
'@opentelemetry/instrumentation-fs': { enabled: false },
|
|
169
|
+
...config.instrumentations,
|
|
170
|
+
[HTTP_INSTRUMENTATION]: {
|
|
171
|
+
...http,
|
|
172
|
+
redactedQueryParams: client,
|
|
173
|
+
redactedQueryParamsServer: server,
|
|
174
|
+
requestHook: (span: unknown, request: unknown) => {
|
|
175
|
+
// A ClientRequest (outgoing) has setHeader(); an IncomingMessage does not.
|
|
176
|
+
const outgoing = typeof (request as { setHeader?: unknown } | null)?.setHeader === 'function';
|
|
177
|
+
redactSpanUrls(span, outgoing ? client : server);
|
|
178
|
+
if (typeof appHook === 'function') appHook(span, request);
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Hosts spans may reach in clear text: this machine, a private network, or a name that only resolves inside one. */
|
|
185
|
+
function isPrivateHost(hostname: string): boolean {
|
|
186
|
+
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
187
|
+
if (host.includes(':')) return host === '::1' || /^f[cd]/.test(host);
|
|
188
|
+
return (
|
|
189
|
+
!host.includes('.') ||
|
|
190
|
+
/\.(localhost|local|internal)$/.test(host) ||
|
|
191
|
+
/^(127|10)\./.test(host) ||
|
|
192
|
+
/^192\.168\./.test(host) ||
|
|
193
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(host)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The OTLP endpoint as it is safe to log, its origin (the path, query or
|
|
199
|
+
* userinfo can carry an API key), and whether spans travel to it in clear
|
|
200
|
+
* text beyond this machine and its private network.
|
|
201
|
+
*/
|
|
202
|
+
export function describeOtelEndpoint(config: OtelConfig): { origin: string; plaintext: boolean } {
|
|
203
|
+
try {
|
|
204
|
+
const url = new URL(config.endpoint || DEFAULT_ENDPOINT);
|
|
205
|
+
return { origin: url.origin, plaintext: url.protocol === 'http:' && !isPrivateHost(url.hostname) };
|
|
206
|
+
} catch {
|
|
207
|
+
return { origin: '(invalid URL)', plaintext: false };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Builds a Resource with whichever API the installed `@opentelemetry/resources`
|
|
213
|
+
* provides: 2.x only exports `resourceFromAttributes()` (`Resource` is a type
|
|
214
|
+
* there, so `new Resource()` throws), 1.x exports the `Resource` class.
|
|
215
|
+
*/
|
|
216
|
+
export function createResource(resources: OtelResourcesModule, attributes: Record<string, string>): unknown {
|
|
217
|
+
if (typeof resources?.resourceFromAttributes === 'function') {
|
|
218
|
+
return resources.resourceFromAttributes(attributes);
|
|
219
|
+
}
|
|
220
|
+
if (typeof resources?.Resource === 'function') {
|
|
221
|
+
return new resources.Resource(attributes);
|
|
222
|
+
}
|
|
223
|
+
throw new Error(
|
|
224
|
+
'[iskra/otel] Unsupported @opentelemetry/resources: it exports neither resourceFromAttributes() nor Resource',
|
|
225
|
+
);
|
|
32
226
|
}
|
|
33
227
|
|
|
34
228
|
/**
|
|
@@ -45,32 +239,29 @@ export async function initOtel(config: OtelConfig, appName: string): Promise<voi
|
|
|
45
239
|
{ OTLPTraceExporter },
|
|
46
240
|
{ OTLPMetricExporter },
|
|
47
241
|
{ PeriodicExportingMetricReader },
|
|
48
|
-
|
|
242
|
+
resources,
|
|
49
243
|
semconv,
|
|
50
244
|
] = await Promise.all([
|
|
51
|
-
importOptional(OTEL_MODULES.sdkNode),
|
|
52
|
-
importOptional(OTEL_MODULES.autoInstrumentations),
|
|
53
|
-
importOptional(OTEL_MODULES.traceExporter),
|
|
54
|
-
importOptional(OTEL_MODULES.metricExporter),
|
|
55
|
-
importOptional(OTEL_MODULES.sdkMetrics),
|
|
56
|
-
importOptional(OTEL_MODULES.resources),
|
|
57
|
-
importOptional(OTEL_MODULES.semconv),
|
|
245
|
+
importOptional<OtelSdkNode>(OTEL_MODULES.sdkNode),
|
|
246
|
+
importOptional<OtelAutoInstrumentations>(OTEL_MODULES.autoInstrumentations),
|
|
247
|
+
importOptional<OtelTraceExporter>(OTEL_MODULES.traceExporter),
|
|
248
|
+
importOptional<OtelMetricExporter>(OTEL_MODULES.metricExporter),
|
|
249
|
+
importOptional<OtelSdkMetrics>(OTEL_MODULES.sdkMetrics),
|
|
250
|
+
importOptional<OtelResourcesModule>(OTEL_MODULES.resources),
|
|
251
|
+
importOptional<OtelSemconv>(OTEL_MODULES.semconv),
|
|
58
252
|
]);
|
|
59
253
|
|
|
60
254
|
const serviceName = config.serviceName || appName;
|
|
61
|
-
const endpoint = config.endpoint ||
|
|
255
|
+
const endpoint = config.endpoint || DEFAULT_ENDPOINT;
|
|
62
256
|
|
|
63
|
-
const resource =
|
|
64
|
-
[semconv.ATTR_SERVICE_NAME]: serviceName,
|
|
65
|
-
[semconv.ATTR_SERVICE_VERSION]: config.serviceVersion || '0.1.0',
|
|
66
|
-
'deployment.environment': config.environment ||
|
|
257
|
+
const resource = createResource(resources, {
|
|
258
|
+
[semconv.ATTR_SERVICE_NAME ?? 'service.name']: serviceName,
|
|
259
|
+
[semconv.ATTR_SERVICE_VERSION ?? 'service.version']: config.serviceVersion || '0.1.0',
|
|
260
|
+
'deployment.environment': config.environment || nodeEnv() || 'development',
|
|
67
261
|
...config.resourceAttributes,
|
|
68
262
|
});
|
|
69
263
|
|
|
70
|
-
const instrumentationOverrides
|
|
71
|
-
'@opentelemetry/instrumentation-fs': { enabled: false },
|
|
72
|
-
...config.instrumentations,
|
|
73
|
-
};
|
|
264
|
+
const instrumentationOverrides = autoInstrumentationOptions(config);
|
|
74
265
|
|
|
75
266
|
const sdk = new NodeSDK({
|
|
76
267
|
resource,
|
|
@@ -84,14 +275,19 @@ export async function initOtel(config: OtelConfig, appName: string): Promise<voi
|
|
|
84
275
|
|
|
85
276
|
sdk.start();
|
|
86
277
|
sdkInstance = sdk;
|
|
87
|
-
} catch (err
|
|
278
|
+
} catch (err) {
|
|
88
279
|
// Provide a clear error when OTel packages are not installed
|
|
89
|
-
|
|
280
|
+
const e = err as { code?: string; message?: string } | null;
|
|
281
|
+
if (
|
|
282
|
+
e?.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
283
|
+
e?.code === 'MODULE_NOT_FOUND' ||
|
|
284
|
+
e?.message?.includes('Cannot find')
|
|
285
|
+
) {
|
|
90
286
|
throw new Error(
|
|
91
287
|
`[iskra/otel] OpenTelemetry packages are not installed. ` +
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
288
|
+
`Install them with: bun add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node ` +
|
|
289
|
+
`@opentelemetry/exporter-trace-otlp-http @opentelemetry/exporter-metrics-otlp-http ` +
|
|
290
|
+
`@opentelemetry/sdk-metrics @opentelemetry/resources @opentelemetry/semantic-conventions`,
|
|
95
291
|
);
|
|
96
292
|
}
|
|
97
293
|
throw err;
|
package/src/traced.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SpanStatusCode, trace, type Attributes, type Span, type Tracer } from '@opentelemetry/api';
|
|
2
|
+
|
|
3
|
+
export interface TracedOptions {
|
|
4
|
+
/** Attributes of every span. */
|
|
5
|
+
attributes?: Attributes;
|
|
6
|
+
/** The tracer; default the global provider's `iskra` tracer (no spans without an OpenTelemetry SDK). */
|
|
7
|
+
tracer?: Tracer;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function fail(span: Span, error: unknown): void {
|
|
11
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
12
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.name : 'Error' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wraps `fn` in a span named `name`, by convention `layer.domain.method`
|
|
17
|
+
* (`repo.usuarios.buscar`): the span is the active one while `fn` runs, so
|
|
18
|
+
* the spans started inside (a query, a call to another service) are its
|
|
19
|
+
* children; a throw or a rejection marks it as an error. Without an
|
|
20
|
+
* OpenTelemetry SDK it costs a no-op span.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const buscar = traced('repo.usuarios.buscar', async (id: number) => oracle.one(SQL, { id }));
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function traced<A extends unknown[], R>(
|
|
27
|
+
name: string,
|
|
28
|
+
fn: (...args: A) => R,
|
|
29
|
+
options: TracedOptions = {},
|
|
30
|
+
): (...args: A) => R {
|
|
31
|
+
return function (this: unknown, ...args: A): R {
|
|
32
|
+
const tracer = options.tracer ?? trace.getTracer('iskra');
|
|
33
|
+
return tracer.startActiveSpan(name, { attributes: options.attributes }, (span) => {
|
|
34
|
+
let result: R;
|
|
35
|
+
try {
|
|
36
|
+
result = fn.apply(this, args);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
fail(span, error);
|
|
39
|
+
span.end();
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
if (result instanceof Promise) {
|
|
43
|
+
return result.then(
|
|
44
|
+
(value) => {
|
|
45
|
+
span.end();
|
|
46
|
+
return value;
|
|
47
|
+
},
|
|
48
|
+
(error: unknown) => {
|
|
49
|
+
fail(span, error);
|
|
50
|
+
span.end();
|
|
51
|
+
throw error;
|
|
52
|
+
},
|
|
53
|
+
) as R;
|
|
54
|
+
}
|
|
55
|
+
span.end();
|
|
56
|
+
return result;
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -13,17 +13,42 @@ export interface Plugin {
|
|
|
13
13
|
install(app: App): Promise<void> | void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** What an `app.on()` handler receives; `payload` is the emitted value. */
|
|
17
|
+
export interface Context<T = unknown> {
|
|
17
18
|
app: App;
|
|
18
19
|
logger: Logger;
|
|
19
20
|
payload: T;
|
|
20
|
-
reply
|
|
21
|
+
/** Emits `<event>:reply` with `data`. */
|
|
22
|
+
reply(data: unknown): void;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/**
|
|
26
|
+
* What each kit shares through `app.context`, by key, so `app.context.get('db')`
|
|
27
|
+
* is typed. Kits add their keys with declaration merging, and so can an app:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* declare module '@iskra-bun/core' {
|
|
31
|
+
* interface AppContextRegistry {
|
|
32
|
+
* bridge: DesktopBridge;
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- filled by declaration merging
|
|
38
|
+
export interface AppContextRegistry {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The payload of each app event, by name, so `app.on('process:exit', ...)`
|
|
42
|
+
* and `app.emit(...)` are typed. Kits add their events with declaration
|
|
43
|
+
* merging, and so can an app (as with AppContextRegistry).
|
|
44
|
+
*/
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- filled by declaration merging
|
|
46
|
+
export interface AppEvents {}
|
|
47
|
+
|
|
23
48
|
export interface OtelConfig {
|
|
24
49
|
/** Defaults to true when otel config is present */
|
|
25
50
|
enabled?: boolean;
|
|
26
|
-
/** OTLP endpoint URL. Defaults to http://localhost:4318 */
|
|
51
|
+
/** OTLP endpoint URL. Defaults to http://localhost:4318; use https:// for a collector on another host. */
|
|
27
52
|
endpoint?: string;
|
|
28
53
|
/** Overrides app name for the service.name resource attribute */
|
|
29
54
|
serviceName?: string;
|
|
@@ -35,8 +60,14 @@ export interface OtelConfig {
|
|
|
35
60
|
metricIntervalMs?: number;
|
|
36
61
|
/** Additional resource attributes */
|
|
37
62
|
resourceAttributes?: Record<string, string>;
|
|
38
|
-
/**
|
|
39
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Options of each auto-instrumentation, by package name, passed to
|
|
65
|
+
* getNodeAutoInstrumentations(): `{ enabled: false }`, or its own options
|
|
66
|
+
* (`'@opentelemetry/instrumentation-http': { ignoreIncomingRequestHook,
|
|
67
|
+
* redactedQueryParams }`). The HTTP one redacts SECRET_QUERY_PARAMS in
|
|
68
|
+
* exported URLs unless `redactedQueryParams` says otherwise.
|
|
69
|
+
*/
|
|
70
|
+
instrumentations?: Record<string, { enabled?: boolean; [option: string]: unknown }>;
|
|
40
71
|
}
|
|
41
72
|
|
|
42
73
|
export interface AppConfig {
|
|
@@ -46,6 +77,13 @@ export interface AppConfig {
|
|
|
46
77
|
level?: string;
|
|
47
78
|
};
|
|
48
79
|
otel?: OtelConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Signals that trigger a graceful `stop()` and exit. Default
|
|
82
|
+
* `['SIGTERM', 'SIGINT']` (none under NODE_ENV=test); `false` disables.
|
|
83
|
+
*/
|
|
84
|
+
shutdownSignals?: string[] | false;
|
|
85
|
+
/** Max time for a signal-triggered stop before forcing exit(1). Default 10000. */
|
|
86
|
+
shutdownTimeoutMs?: number;
|
|
49
87
|
processes?: Record<string, ProcessConfig>;
|
|
50
88
|
socket?: {
|
|
51
89
|
enabled: boolean;
|
|
@@ -53,15 +91,17 @@ export interface AppConfig {
|
|
|
53
91
|
adapter?: 'bun' | 'socket.io';
|
|
54
92
|
};
|
|
55
93
|
kv?: {
|
|
56
|
-
driver: 'memory' | 'redis'
|
|
57
|
-
|
|
94
|
+
driver: 'memory' | 'redis';
|
|
95
|
+
/** Redis: a URL string, ioredis options, or ioredis options with `url`. */
|
|
96
|
+
connection?: string | Record<string, unknown>;
|
|
58
97
|
};
|
|
59
98
|
db?: {
|
|
60
99
|
driver: 'postgres' | 'mysql' | 'sqlite' | 'libsql';
|
|
61
100
|
url: string;
|
|
62
101
|
authToken?: string;
|
|
63
102
|
};
|
|
64
|
-
|
|
103
|
+
/** Sections of other kits or of the app; read them with their own type. */
|
|
104
|
+
[key: string]: unknown;
|
|
65
105
|
}
|
|
66
106
|
|
|
67
107
|
export interface RestartBackoffConfig {
|
|
@@ -80,7 +120,20 @@ export interface ProcessConfig {
|
|
|
80
120
|
restartOnCrash?: boolean;
|
|
81
121
|
maxRestarts?: number;
|
|
82
122
|
restartCooldown?: number;
|
|
123
|
+
/** Variables set for the child, over the ones it inherits (see `inheritEnv`). */
|
|
83
124
|
env?: Record<string, string>;
|
|
125
|
+
/**
|
|
126
|
+
* Which of the app's environment variables the child inherits. Default
|
|
127
|
+
* `false`: a minimal set without secrets (PATH, HOME, locale, TZ, temp
|
|
128
|
+
* dir, NODE_ENV…). A list adds those names to it; `true` passes them all
|
|
129
|
+
* (DATABASE_URL, AUTH_SECRET, cloud keys…).
|
|
130
|
+
*/
|
|
131
|
+
inheritEnv?: boolean | string[];
|
|
132
|
+
/**
|
|
133
|
+
* `stdio` mode: bytes `send()` lets wait for a child that is not reading
|
|
134
|
+
* its stdin; past this it refuses messages (returns false). Default 8 MiB.
|
|
135
|
+
*/
|
|
136
|
+
maxPendingStdinBytes?: number;
|
|
84
137
|
/** Exponential backoff settings for restarts. Defaults to 1000 ms flat (no backoff). */
|
|
85
138
|
restartBackoff?: RestartBackoffConfig;
|
|
86
139
|
}
|