unnbound-logger-sdk 3.2.0-beta.1 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +22 -2
- package/dist/axios.d.ts +6 -1
- package/dist/axios.js +31 -22
- package/dist/emf.d.ts +2 -1
- package/dist/encode.d.ts +5 -0
- package/dist/encode.js +7 -1
- package/dist/index.d.ts +2 -7
- package/dist/index.js +4 -7
- package/dist/internal.d.ts +10 -2
- package/dist/internal.js +1 -1
- package/dist/logger.d.ts +7 -6
- package/dist/logger.js +108 -36
- package/dist/middleware.js +1 -0
- package/dist/redaction.d.ts +3 -0
- package/dist/redaction.js +135 -0
- package/dist/span.d.ts +2 -4
- package/dist/span.js +4 -7
- package/dist/storage.d.ts +17 -1
- package/dist/storage.js +94 -4
- package/dist/trace.d.ts +2 -15
- package/dist/trace.js +3 -16
- package/dist/types.d.ts +5 -12
- package/dist/utils.d.ts +8 -17
- package/dist/utils.js +4 -22
- package/package.json +5 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## [3.0.34](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-logger-sdk-v3.0.33...unnbound-logger-sdk-v3.0.34) (2025-11-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* setup ci/cd ([dd2640f](https://github.com/unnbounddev/unnbound-sdks/commit/dd2640f01e39f1342f3e08dc882164b50331c7bb))
|
|
7
|
+
|
|
8
|
+
## [3.0.33](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-logger-sdk-v3.0.32...unnbound-logger-sdk-v3.0.33) (2025-11-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* setup ci/cd ([520c03a](https://github.com/unnbounddev/unnbound-sdks/commit/520c03aacd978d7923b3bcf45c4f74a870be019d))
|
|
14
|
+
|
|
15
|
+
## [3.0.32](https://github.com/unnbounddev/unnbound-sdks/compare/unnbound-logger-sdk-v3.0.31...unnbound-logger-sdk-v3.0.32) (2025-11-25)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* setup ci/cd ([d3759c1](https://github.com/unnbounddev/unnbound-sdks/commit/d3759c14f7863e4c8d2f5e80bdbbf2446df79f1a))
|
|
21
|
+
* setup ci/cd ([7e06fc8](https://github.com/unnbounddev/unnbound-sdks/commit/7e06fc84ae7bafba3e9c2c0225f9f9d614fa9207))
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ interface Log<T extends LogType = 'general'> {
|
|
|
44
44
|
serviceId?: string; // From UNNBOUND_SERVICE_ID environment variable
|
|
45
45
|
deploymentId?: string; // From UNNBOUND_DEPLOYMENT_ID environment variable
|
|
46
46
|
workflowId?: string; // From UNNBOUND_WORKFLOW_ID environment variable
|
|
47
|
-
environment?: string; // From WORKFLOW_ENV, UNNBOUND_ENVIRONMENT, or ENVIRONMENT, whichever is set first
|
|
47
|
+
environment?: string; // From TEMPER_ENVIRONMENT, WORKFLOW_ENV, UNNBOUND_ENVIRONMENT, or ENVIRONMENT, whichever is set first
|
|
48
48
|
err?: unknown; // Only present for Error objects
|
|
49
49
|
duration?: number; // Duration in milliseconds for span operations
|
|
50
50
|
http?: T extends 'http' ? HttpPayload : never;
|
|
@@ -148,6 +148,26 @@ logger.error({ err: new Error('Connection refused') }, 'Failed to sync order.');
|
|
|
148
148
|
Errors are serialized by Pino's `stdSerializers.err`, which emits `type`, `message`, and `stack`.
|
|
149
149
|
Unlike the other levels, `error` has no single-argument overload, so the message is required.
|
|
150
150
|
|
|
151
|
+
### Redaction Masks
|
|
152
|
+
|
|
153
|
+
`registerRedactionMasks` replaces each registered string with `[Redacted]` wherever it appears in a log line: messages, fields, errors, and child-logger bindings. `@ontemper/resources` registers the secrets it resolves; register other secret values yourself:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { logger, registerRedactionMasks } from 'unnbound-logger-sdk';
|
|
157
|
+
|
|
158
|
+
registerRedactionMasks([apiKey]);
|
|
159
|
+
const partnerLog = logger.child({ partner: 'acme', auth: `Bearer ${apiKey}` });
|
|
160
|
+
partnerLog.info('Calling partner.');
|
|
161
|
+
// Result: { ..., "partner": "acme", "auth": "Bearer [Redacted]", "message": "Calling partner." }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- Masks replace text in string values only. A property name that contains a secret is written unchanged.
|
|
165
|
+
- Masks apply to the finished line when it is written. A mask registered after `child()` or `setBindings()` still redacts those bindings, and the output of custom serializers and formatters is masked too.
|
|
166
|
+
- A mask registered inside a request scope applies to the whole scope, and stays active for the rest of the process after the scope ends. The process keeps at most 4096 of these retained masks and drops the oldest first; masks held by an active scope are not capped.
|
|
167
|
+
- A value that code transforms before logging (for example, Base64-encodes) no longer matches its mask.
|
|
168
|
+
|
|
169
|
+
Treat masking as a safeguard, not a guarantee: do not log secrets.
|
|
170
|
+
|
|
151
171
|
## HTTP Request/Response Logging
|
|
152
172
|
|
|
153
173
|
```typescript
|
|
@@ -464,5 +484,5 @@ console.log(traceId); // "550e8400-e29b-41d4-a716-446655440000"
|
|
|
464
484
|
- `UNNBOUND_SERVICE_ID` - Service identifier (included in all logs)
|
|
465
485
|
- `UNNBOUND_DEPLOYMENT_ID` - Deployment identifier (included in all logs)
|
|
466
486
|
- `UNNBOUND_WORKFLOW_URL` - Base URL for webhook endpoint logging
|
|
467
|
-
- `WORKFLOW_ENV`, `UNNBOUND_ENVIRONMENT`, `ENVIRONMENT` - Environment name, read in that order (included in all logs)
|
|
487
|
+
- `TEMPER_ENVIRONMENT`, `WORKFLOW_ENV`, `UNNBOUND_ENVIRONMENT`, `ENVIRONMENT` - Environment name, read in that order (included in all logs). Temper injects `TEMPER_ENVIRONMENT`; the other names are legacy fallbacks.
|
|
468
488
|
- `LOG_LEVEL` - Log level (default: 'debug')
|
package/dist/axios.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
|
|
2
|
+
import { startSpan } from './span';
|
|
2
3
|
import { type HttpOptions } from './types';
|
|
3
4
|
type GetPayload = (config: AxiosRequestConfig, res?: AxiosResponse) => object;
|
|
4
5
|
type RedactOptions = {
|
|
6
|
+
request?: boolean;
|
|
7
|
+
requestHeaders?: boolean;
|
|
5
8
|
response?: boolean;
|
|
9
|
+
responseHeaders?: boolean;
|
|
6
10
|
};
|
|
7
11
|
export type OnRequest = (config: AxiosRequestConfig, callback: (config: AxiosRequestConfig) => Promise<AxiosResponse<any>>) => Promise<AxiosResponse<any>>;
|
|
8
12
|
interface HttpClientOptions extends HttpOptions<GetPayload> {
|
|
@@ -15,6 +19,7 @@ interface HttpClientOptions extends HttpOptions<GetPayload> {
|
|
|
15
19
|
* marker, suppression does not depend on runtime env vars.
|
|
16
20
|
*/
|
|
17
21
|
silent?: boolean;
|
|
22
|
+
startSpan?: typeof startSpan;
|
|
18
23
|
}
|
|
19
24
|
/**
|
|
20
25
|
* Wraps an axios instance to add tracing and span tracking.
|
|
@@ -24,5 +29,5 @@ interface HttpClientOptions extends HttpOptions<GetPayload> {
|
|
|
24
29
|
* @param options - Configuration options for HTTP tracing
|
|
25
30
|
* @returns The wrapped axios instance with span tracking
|
|
26
31
|
*/
|
|
27
|
-
export declare const traceAxios: (client: AxiosInstance, { ignoreTraceRoutes, traceHeaderKey, getPayload, onRequest, redact: redactOptions, silent, }?: HttpClientOptions) => AxiosInstance;
|
|
32
|
+
export declare const traceAxios: (client: AxiosInstance, { ignoreTraceRoutes, traceHeaderKey, getPayload, onRequest, redact: redactOptions, silent, startSpan: startSpanImpl, }?: HttpClientOptions) => AxiosInstance;
|
|
28
33
|
export {};
|
package/dist/axios.js
CHANGED
|
@@ -16,25 +16,29 @@ const extractQueryParams = (url, config) => {
|
|
|
16
16
|
keys.forEach((key) => object.searchParams.delete(key));
|
|
17
17
|
return { url: object.toString(), query: keys.length > 0 ? query : undefined };
|
|
18
18
|
};
|
|
19
|
-
const buildOutgoingHttpPayload = (config, res, redact) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
const buildOutgoingHttpPayload = (config, res, redact) => {
|
|
20
|
+
const { url, query } = extractQueryParams(buildUrl(config.baseURL, config.url), config);
|
|
21
|
+
return {
|
|
22
|
+
type: 'http',
|
|
23
|
+
http: {
|
|
24
|
+
url,
|
|
25
|
+
query: redact?.request && query ? '[Redacted]' : query,
|
|
26
|
+
method: config.method?.toLowerCase() ?? 'get',
|
|
27
|
+
incoming: false,
|
|
28
|
+
request: {
|
|
29
|
+
headers: redact?.request || redact?.requestHeaders ? '[Redacted]' : config.headers,
|
|
30
|
+
body: redact?.request ? '[Redacted]' : (0, utils_1.safeJsonParse)(config.data),
|
|
31
|
+
},
|
|
32
|
+
response: res
|
|
33
|
+
? {
|
|
34
|
+
headers: redact?.response || redact?.responseHeaders ? '[Redacted]' : res.headers,
|
|
35
|
+
status: res.status,
|
|
36
|
+
body: redact?.response ? '[Redacted]' : (0, utils_1.safeJsonParse)(res.data),
|
|
37
|
+
}
|
|
38
|
+
: undefined,
|
|
28
39
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
headers: res?.headers,
|
|
32
|
-
status: res.status,
|
|
33
|
-
body: redact?.response ? '[Redacted]' : (0, utils_1.safeJsonParse)(res.data),
|
|
34
|
-
}
|
|
35
|
-
: undefined,
|
|
36
|
-
},
|
|
37
|
-
});
|
|
40
|
+
};
|
|
41
|
+
};
|
|
38
42
|
const getNoopPayload = () => ({});
|
|
39
43
|
const onNoopRequest = (config, callback) => callback(config);
|
|
40
44
|
const tracedMarker = Symbol.for('unnbound-logger-sdk.traceAxios.traced');
|
|
@@ -46,7 +50,7 @@ const tracedMarker = Symbol.for('unnbound-logger-sdk.traceAxios.traced');
|
|
|
46
50
|
* @param options - Configuration options for HTTP tracing
|
|
47
51
|
* @returns The wrapped axios instance with span tracking
|
|
48
52
|
*/
|
|
49
|
-
const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes, traceHeaderKey = types_1.defaultTraceHeaderKey, getPayload = getNoopPayload, onRequest = onNoopRequest, redact: redactOptions, silent = false, } = {
|
|
53
|
+
const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes, traceHeaderKey = types_1.defaultTraceHeaderKey, getPayload = getNoopPayload, onRequest = onNoopRequest, redact: redactOptions, silent = false, startSpan: startSpanImpl = span_1.startSpan, } = {
|
|
50
54
|
ignoreTraceRoutes: types_1.defaultIgnoreTraceRoutes,
|
|
51
55
|
traceHeaderKey: types_1.defaultTraceHeaderKey,
|
|
52
56
|
getPayload: getNoopPayload,
|
|
@@ -54,7 +58,9 @@ const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRout
|
|
|
54
58
|
}) => {
|
|
55
59
|
if (tracedMarker in client)
|
|
56
60
|
return client;
|
|
57
|
-
const redact =
|
|
61
|
+
const redact = redactOptions === true || redactOptions === false
|
|
62
|
+
? { request: redactOptions, response: redactOptions }
|
|
63
|
+
: redactOptions;
|
|
58
64
|
const createSpanWrappedRequest = (originalMethod, method) => {
|
|
59
65
|
const { headers: defaultHeaders, ...partialDefaultConfig } = client.defaults;
|
|
60
66
|
const headers = { ...defaultHeaders.common, ...(method && defaultHeaders[method]) };
|
|
@@ -67,11 +73,14 @@ const traceAxios = (client, { ignoreTraceRoutes = types_1.defaultIgnoreTraceRout
|
|
|
67
73
|
return originalMethod(config);
|
|
68
74
|
const traceId = storage_1.storage.getStore()?.traceId;
|
|
69
75
|
config = { ...config, headers: { ...config.headers, [traceHeaderKey]: traceId } };
|
|
70
|
-
if (silent)
|
|
76
|
+
if (silent) {
|
|
77
|
+
// SAFETY: onRequest returns the originalMethod promise, typed as Axios R
|
|
71
78
|
return onRequest(config, (config) => originalMethod(config));
|
|
79
|
+
}
|
|
80
|
+
// SAFETY: startSpan resolves to the originalMethod promise, typed as Axios R
|
|
72
81
|
return onRequest(config, (config) => {
|
|
73
82
|
// Execute the request within a span
|
|
74
|
-
return (
|
|
83
|
+
return startSpanImpl('Outgoing HTTP request', () => originalMethod(config), (options) => {
|
|
75
84
|
const response = options
|
|
76
85
|
? options.error
|
|
77
86
|
? (0, axios_1.isAxiosError)(options.error)
|
package/dist/emf.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Bindings } from 'pino';
|
|
1
2
|
/**
|
|
2
3
|
* CloudWatch Embedded Metric Format (EMF) support.
|
|
3
4
|
*
|
|
@@ -18,7 +19,7 @@ export interface EmitMetricOptions {
|
|
|
18
19
|
/** Dimension names to use for grouping (keys from dimensions object) */
|
|
19
20
|
dimensionKeys?: string[];
|
|
20
21
|
/** Additional properties to include in the log */
|
|
21
|
-
properties?:
|
|
22
|
+
properties?: Bindings;
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
25
|
* Emit a metric via CloudWatch EMF format.
|
package/dist/encode.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log records are read one line at a time — container stdout becomes one CloudWatch event per
|
|
3
|
+
* line and the log forwarder JSON-parses each event — so LF becomes CR to keep a value from
|
|
4
|
+
* splitting its own record. `whitespace-pre-wrap` still renders it as a break; `decode` restores LF.
|
|
5
|
+
*/
|
|
1
6
|
export declare const encode: (value: string) => string;
|
|
2
7
|
export declare const decode: (value: string) => string;
|
package/dist/encode.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.decode = exports.encode = void 0;
|
|
4
|
-
const
|
|
4
|
+
const redaction_1 = require("./redaction");
|
|
5
|
+
/**
|
|
6
|
+
* Log records are read one line at a time — container stdout becomes one CloudWatch event per
|
|
7
|
+
* line and the log forwarder JSON-parses each event — so LF becomes CR to keep a value from
|
|
8
|
+
* splitting its own record. `whitespace-pre-wrap` still renders it as a break; `decode` restores LF.
|
|
9
|
+
*/
|
|
10
|
+
const encode = (value) => (0, redaction_1.applyRedactionMasks)(value).replaceAll('\n', '\r');
|
|
5
11
|
exports.encode = encode;
|
|
6
12
|
const decode = (value) => value.replaceAll('\r', '\n');
|
|
7
13
|
exports.decode = decode;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* unnbound-logger
|
|
3
|
-
*
|
|
4
|
-
* A structured logging library built on Pino with TypeScript support.
|
|
5
|
-
* Provides consistent, well-typed logging across different operational contexts.
|
|
6
|
-
*/
|
|
7
1
|
export { type OnRequest, traceAxios } from './axios';
|
|
8
2
|
export type { EmitMetricOptions, MetricDefinition, MetricUnit } from './emf';
|
|
9
3
|
export { emitMetric, emitMetrics } from './emf';
|
|
@@ -11,9 +5,10 @@ export { decode, encode } from './encode';
|
|
|
11
5
|
export type { ILogger, UnnboundLogger } from './logger';
|
|
12
6
|
export { logger } from './logger';
|
|
13
7
|
export { traceMiddleware } from './middleware';
|
|
8
|
+
export { applyRedactionMasks, registerRedactionMasks } from './redaction';
|
|
14
9
|
export { startSpan } from './span';
|
|
15
10
|
export type { UnnboundContext } from './storage';
|
|
16
11
|
export { storage, withStorage } from './storage';
|
|
17
12
|
export { getTraceId, withTrace } from './trace';
|
|
18
|
-
export type { As2Operation, As2Outcome, As2Payload,
|
|
13
|
+
export type { As2Operation, As2Outcome, As2Payload, EdiFormat, EdiOperation, EdiPayload, EdiX12Operation, EdiX12Payload, EventSource, HttpMethod, HttpPayload, Log, LogLevel, LogType, Maybe, SftpPayload, } from './types';
|
|
19
14
|
export { defaultMessageHeaderKey, defaultTraceHeaderKey } from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* unnbound-logger
|
|
4
|
-
*
|
|
5
|
-
* A structured logging library built on Pino with TypeScript support.
|
|
6
|
-
* Provides consistent, well-typed logging across different operational contexts.
|
|
7
|
-
*/
|
|
8
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.defaultTraceHeaderKey = exports.defaultMessageHeaderKey = exports.withTrace = exports.getTraceId = exports.withStorage = exports.storage = exports.startSpan = exports.traceMiddleware = exports.logger = exports.encode = exports.decode = exports.emitMetrics = exports.emitMetric = exports.traceAxios = void 0;
|
|
3
|
+
exports.defaultTraceHeaderKey = exports.defaultMessageHeaderKey = exports.withTrace = exports.getTraceId = exports.withStorage = exports.storage = exports.startSpan = exports.registerRedactionMasks = exports.applyRedactionMasks = exports.traceMiddleware = exports.logger = exports.encode = exports.decode = exports.emitMetrics = exports.emitMetric = exports.traceAxios = void 0;
|
|
10
4
|
var axios_1 = require("./axios");
|
|
11
5
|
Object.defineProperty(exports, "traceAxios", { enumerable: true, get: function () { return axios_1.traceAxios; } });
|
|
12
6
|
var emf_1 = require("./emf");
|
|
@@ -19,6 +13,9 @@ var logger_1 = require("./logger");
|
|
|
19
13
|
Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return logger_1.logger; } });
|
|
20
14
|
var middleware_1 = require("./middleware");
|
|
21
15
|
Object.defineProperty(exports, "traceMiddleware", { enumerable: true, get: function () { return middleware_1.traceMiddleware; } });
|
|
16
|
+
var redaction_1 = require("./redaction");
|
|
17
|
+
Object.defineProperty(exports, "applyRedactionMasks", { enumerable: true, get: function () { return redaction_1.applyRedactionMasks; } });
|
|
18
|
+
Object.defineProperty(exports, "registerRedactionMasks", { enumerable: true, get: function () { return redaction_1.registerRedactionMasks; } });
|
|
22
19
|
var span_1 = require("./span");
|
|
23
20
|
Object.defineProperty(exports, "startSpan", { enumerable: true, get: function () { return span_1.startSpan; } });
|
|
24
21
|
var storage_1 = require("./storage");
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
interface InternalLogEntry {
|
|
2
|
+
_internal: true;
|
|
3
|
+
_hide?: true;
|
|
4
|
+
}
|
|
5
|
+
type LogEntry = object & {
|
|
6
|
+
_hide?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const hidden: (data: LogEntry) => boolean;
|
|
2
9
|
/** @public - used by SDK consumers via dist/internal */
|
|
3
|
-
export declare const internal: () =>
|
|
10
|
+
export declare const internal: () => InternalLogEntry;
|
|
11
|
+
export {};
|
package/dist/internal.js
CHANGED
|
@@ -5,7 +5,7 @@ const o = { _internal: true };
|
|
|
5
5
|
// Sandbox-internal logs are hidden from customer-visible streams.
|
|
6
6
|
if (process.env.UNNBOUND_IDLE_TIMEOUT)
|
|
7
7
|
o._hide = true;
|
|
8
|
-
const hidden = (data) =>
|
|
8
|
+
const hidden = (data) => data._hide === true;
|
|
9
9
|
exports.hidden = hidden;
|
|
10
10
|
/** @public - used by SDK consumers via dist/internal */
|
|
11
11
|
const internal = () => o;
|
package/dist/logger.d.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import pino from 'pino';
|
|
2
|
-
import type { DestinationStream } from 'pino';
|
|
2
|
+
import type { Bindings, ChildLoggerOptions, DestinationStream } from 'pino';
|
|
3
3
|
export interface ILogger {
|
|
4
|
-
trace(object:
|
|
4
|
+
trace(object: Bindings, message: string): void;
|
|
5
5
|
trace(message: string): void;
|
|
6
|
-
debug(object:
|
|
6
|
+
debug(object: Bindings, message: string): void;
|
|
7
7
|
debug(message: string): void;
|
|
8
|
-
info(object:
|
|
8
|
+
info(object: Bindings, message: string): void;
|
|
9
9
|
info(message: string): void;
|
|
10
|
-
warn(object:
|
|
10
|
+
warn(object: Bindings, message: string): void;
|
|
11
11
|
warn(message: string): void;
|
|
12
12
|
error<O extends {
|
|
13
13
|
err: unknown;
|
|
14
14
|
}>(object: O, message: string): void;
|
|
15
15
|
}
|
|
16
|
-
export interface UnnboundLogger extends ILogger, Omit<pino.Logger, keyof ILogger> {
|
|
16
|
+
export interface UnnboundLogger extends ILogger, Omit<pino.Logger, keyof ILogger | 'child'> {
|
|
17
|
+
child(bindings: Bindings, options?: ChildLoggerOptions): UnnboundLogger;
|
|
17
18
|
}
|
|
18
19
|
export declare const createLogger: (destination?: DestinationStream) => UnnboundLogger;
|
|
19
20
|
export declare const logger: UnnboundLogger;
|
package/dist/logger.js
CHANGED
|
@@ -4,23 +4,40 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.logger = exports.createLogger = void 0;
|
|
7
|
+
const axios_1 = require("axios");
|
|
7
8
|
const pino_1 = __importDefault(require("pino"));
|
|
8
9
|
const uuid_1 = require("uuid");
|
|
9
10
|
const encode_1 = require("./encode");
|
|
10
11
|
const internal_1 = require("./internal");
|
|
12
|
+
const redaction_1 = require("./redaction");
|
|
11
13
|
const storage_1 = require("./storage");
|
|
12
|
-
|
|
13
|
-
const workflowEnvironment = process.env.
|
|
14
|
+
// The Temper name wins while legacy fallbacks keep existing workflow images compatible.
|
|
15
|
+
const workflowEnvironment = process.env.TEMPER_ENVIRONMENT ??
|
|
16
|
+
process.env.WORKFLOW_ENV ??
|
|
17
|
+
process.env.UNNBOUND_ENVIRONMENT ??
|
|
18
|
+
process.env.ENVIRONMENT;
|
|
14
19
|
const formatLevel = (level) => ({ level });
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return
|
|
23
|
-
|
|
20
|
+
const isBindings = (value) => Object(value) === value;
|
|
21
|
+
const isLogLevel = (value) => value === 'debug' || value === 'info' || value === 'warn' || value === 'error';
|
|
22
|
+
const isStringAnnotation = (value) => {
|
|
23
|
+
try {
|
|
24
|
+
return String.prototype.valueOf.call(value) === value;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// An AxiosError carries its config, the Node request (whose `_header` holds every request header, credentials
|
|
31
|
+
// included), and the response. Those three are dropped; the rest of the standard error fields stay (`type`, `message`,
|
|
32
|
+
// `stack`, `code`, `status`). `traceAxios` logs the exchange itself under its own redaction.
|
|
33
|
+
const serializeError = (error) => {
|
|
34
|
+
const serialized = pino_1.default.stdSerializers.err(error);
|
|
35
|
+
if (!(0, axios_1.isAxiosError)(error) || !isBindings(serialized))
|
|
36
|
+
return serialized;
|
|
37
|
+
const { config: _config, request: _request, response: _response, ...kept } = serialized;
|
|
38
|
+
return kept;
|
|
39
|
+
};
|
|
40
|
+
const copyLogFields = (log, copy, copies, preserveErrorSerializer) => {
|
|
24
41
|
for (const key in log) {
|
|
25
42
|
if (!Object.hasOwn(log, key))
|
|
26
43
|
continue;
|
|
@@ -29,27 +46,65 @@ const formatLog = (log, visited = new WeakSet()) => {
|
|
|
29
46
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype')
|
|
30
47
|
continue;
|
|
31
48
|
const value = log[key];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// We can't encode immutable properties.
|
|
36
|
-
if (!Object.getOwnPropertyDescriptor(log, key)?.writable)
|
|
37
|
-
continue;
|
|
38
|
-
if (typeof value === 'string') {
|
|
39
|
-
log[key] = (0, encode_1.encode)(value);
|
|
40
|
-
}
|
|
41
|
-
else if (Array.isArray(value)) {
|
|
42
|
-
log[key] = value.map((item) => typeof item === 'string'
|
|
43
|
-
? (0, encode_1.encode)(item)
|
|
44
|
-
: item && typeof item === 'object'
|
|
45
|
-
? formatLog(item, visited)
|
|
46
|
-
: item);
|
|
47
|
-
}
|
|
48
|
-
else if (value !== null && typeof value === 'object') {
|
|
49
|
-
log[key] = formatLog(value, visited);
|
|
49
|
+
if (value !== undefined) {
|
|
50
|
+
copy[key] =
|
|
51
|
+
preserveErrorSerializer && key === 'err' && value instanceof Error ? value : copyLogValue(value, copies);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
|
-
return
|
|
54
|
+
return copy;
|
|
55
|
+
};
|
|
56
|
+
const copyLogValue = (value, copies) => {
|
|
57
|
+
if (isStringAnnotation(value))
|
|
58
|
+
return (0, encode_1.encode)(value);
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
const existing = copies.get(value);
|
|
61
|
+
if (Array.isArray(existing))
|
|
62
|
+
return existing;
|
|
63
|
+
const copy = [];
|
|
64
|
+
copies.set(value, copy);
|
|
65
|
+
for (const item of value)
|
|
66
|
+
copy.push(copyLogValue(item, copies));
|
|
67
|
+
return copy;
|
|
68
|
+
}
|
|
69
|
+
if (value instanceof Error) {
|
|
70
|
+
const serialized = serializeError(value);
|
|
71
|
+
const copy = {};
|
|
72
|
+
copies.set(value, copy);
|
|
73
|
+
copies.set(serialized, copy);
|
|
74
|
+
return copyLogFields(serialized, copy, copies, false);
|
|
75
|
+
}
|
|
76
|
+
if (isBindings(value) && Object.prototype.toString.call(value) === '[object Object]') {
|
|
77
|
+
return copyBindings(value, copies, false);
|
|
78
|
+
}
|
|
79
|
+
// Class instances such as AxiosHeaders serialize through toJSON, so the copy walks that output.
|
|
80
|
+
if (isBindings(value) && value.toJSON instanceof Function) {
|
|
81
|
+
const existing = copies.get(value);
|
|
82
|
+
if (existing !== undefined)
|
|
83
|
+
return existing;
|
|
84
|
+
const copy = copyLogValue(value.toJSON(), copies);
|
|
85
|
+
copies.set(value, copy);
|
|
86
|
+
return copy;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
};
|
|
90
|
+
const copyBindings = (log, copies, preserveErrorSerializer) => {
|
|
91
|
+
// Pino hands serializers whatever the caller passed; a primitive is not a WeakMap key.
|
|
92
|
+
if (!isBindings(log))
|
|
93
|
+
return log;
|
|
94
|
+
const existing = copies.get(log);
|
|
95
|
+
if (isBindings(existing))
|
|
96
|
+
return existing;
|
|
97
|
+
const copy = {};
|
|
98
|
+
copies.set(log, copy);
|
|
99
|
+
return copyLogFields(log, copy, copies, preserveErrorSerializer);
|
|
100
|
+
};
|
|
101
|
+
const formatLog = (log) => copyBindings(log, new WeakMap(), true);
|
|
102
|
+
// `err` may be anything thrown; pino returns non-Error values unchanged, so only objects are copied.
|
|
103
|
+
const formatError = (error) => {
|
|
104
|
+
const serialized = serializeError(error);
|
|
105
|
+
if (isStringAnnotation(serialized))
|
|
106
|
+
return (0, encode_1.encode)(serialized);
|
|
107
|
+
return isBindings(serialized) ? formatLog(serialized) : serialized;
|
|
53
108
|
};
|
|
54
109
|
const loggerOptions = {
|
|
55
110
|
level: process.env.LOG_LEVEL ?? 'debug',
|
|
@@ -64,30 +119,33 @@ const loggerOptions = {
|
|
|
64
119
|
serializers: {
|
|
65
120
|
req: pino_1.default.stdSerializers.req,
|
|
66
121
|
res: pino_1.default.stdSerializers.res,
|
|
67
|
-
err:
|
|
122
|
+
err: formatError,
|
|
68
123
|
message: encode_1.encode,
|
|
69
124
|
},
|
|
70
125
|
// Let CloudWatch handle timestamps
|
|
71
126
|
timestamp: false,
|
|
72
127
|
// Change message field from 'msg' to 'message'
|
|
73
128
|
messageKey: 'message',
|
|
74
|
-
formatters: { level: formatLevel, log: formatLog },
|
|
129
|
+
formatters: { bindings: formatLog, level: formatLevel, log: formatLog },
|
|
75
130
|
hooks: {
|
|
76
131
|
logMethod(args, method) {
|
|
77
132
|
const firstArg = args[0];
|
|
78
133
|
let activeMethod = method;
|
|
79
|
-
if (
|
|
134
|
+
if (isBindings(firstArg)) {
|
|
80
135
|
// If the log entry is considered hidden, don't log it
|
|
81
136
|
if ((0, internal_1.hidden)(firstArg))
|
|
82
137
|
return;
|
|
83
138
|
// Dynamic log level by allowing overwriting the log level
|
|
84
|
-
|
|
85
|
-
|
|
139
|
+
const requestedLevel = firstArg.level;
|
|
140
|
+
if (isLogLevel(requestedLevel)) {
|
|
141
|
+
activeMethod = this[requestedLevel];
|
|
86
142
|
firstArg.level = undefined;
|
|
87
143
|
}
|
|
88
144
|
}
|
|
89
145
|
activeMethod.apply(this, args);
|
|
90
146
|
},
|
|
147
|
+
// Masks registered after a value was serialized (child bindings, formatter output) still reach the line.
|
|
148
|
+
streamWrite: redaction_1.redactLogLine,
|
|
91
149
|
},
|
|
92
150
|
redact: {
|
|
93
151
|
paths: [
|
|
@@ -95,12 +153,26 @@ const loggerOptions = {
|
|
|
95
153
|
'http.response.headers.authorization',
|
|
96
154
|
'http.request.headers.Authorization',
|
|
97
155
|
'http.response.headers.Authorization',
|
|
156
|
+
// The EDI client presents its gateway credential in this header, so it joins
|
|
157
|
+
// the Authorization paths: request evidence keeps the URL/status, never the key.
|
|
158
|
+
'http.request.headers.ocp-apim-subscription-key',
|
|
159
|
+
'http.request.headers.Ocp-Apim-Subscription-Key',
|
|
98
160
|
'err.config',
|
|
99
161
|
],
|
|
100
162
|
remove: true,
|
|
101
163
|
},
|
|
102
164
|
};
|
|
103
|
-
|
|
165
|
+
// Pino children inherit the parent's own methods through Object.create, so every logger gets its own
|
|
166
|
+
// wrappers bound to itself; otherwise a child's `child`/`setBindings` would act on its parent.
|
|
167
|
+
const withRedaction = (instance, child, setBindings) => {
|
|
168
|
+
instance.child = (bindings, options) => withRedaction(child.call(instance, formatLog(bindings), options), child, setBindings);
|
|
169
|
+
instance.setBindings = (bindings) => setBindings.call(instance, formatLog(bindings));
|
|
170
|
+
return instance;
|
|
171
|
+
};
|
|
172
|
+
const createLogger = (destination) => {
|
|
173
|
+
const instance = destination ? (0, pino_1.default)(loggerOptions, destination) : (0, pino_1.default)(loggerOptions);
|
|
174
|
+
return withRedaction(instance, instance.child, instance.setBindings);
|
|
175
|
+
};
|
|
104
176
|
exports.createLogger = createLogger;
|
|
105
177
|
exports.logger = (0, exports.createLogger)();
|
|
106
178
|
process.on('uncaughtException', (err, origin) => {
|
package/dist/middleware.js
CHANGED
|
@@ -75,6 +75,7 @@ const traceMiddleware = ({ ignoreTraceRoutes = types_1.defaultIgnoreTraceRoutes,
|
|
|
75
75
|
const response = o
|
|
76
76
|
? {
|
|
77
77
|
status: res.statusCode,
|
|
78
|
+
// SAFETY: Express OutgoingHttpHeaders values are stringified by the tracer
|
|
78
79
|
headers: res.getHeaders(),
|
|
79
80
|
body: res.locals.body,
|
|
80
81
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRedactionMasks = registerRedactionMasks;
|
|
4
|
+
exports.applyRedactionMasks = applyRedactionMasks;
|
|
5
|
+
exports.redactLogLine = redactLogLine;
|
|
6
|
+
const storage_1 = require("./storage");
|
|
7
|
+
const REDACTED = '[Redacted]';
|
|
8
|
+
function registerRedactionMasks(values) {
|
|
9
|
+
const masks = (0, storage_1.getRedactionMasks)() ?? storage_1.processRedactionMasks;
|
|
10
|
+
for (const value of values) {
|
|
11
|
+
if (value.length > 0)
|
|
12
|
+
masks.add(value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const indexMasks = (masks) => {
|
|
16
|
+
const root = { edge: '', end: false, children: new Map() };
|
|
17
|
+
for (const mask of masks) {
|
|
18
|
+
let node = root;
|
|
19
|
+
let rest = mask;
|
|
20
|
+
while (rest.length > 0) {
|
|
21
|
+
const child = node.children.get(rest.charCodeAt(0));
|
|
22
|
+
if (!child) {
|
|
23
|
+
node.children.set(rest.charCodeAt(0), { edge: rest, end: true, children: new Map() });
|
|
24
|
+
rest = '';
|
|
25
|
+
node = root;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
let common = 1;
|
|
29
|
+
while (common < child.edge.length && child.edge.charCodeAt(common) === rest.charCodeAt(common))
|
|
30
|
+
common += 1;
|
|
31
|
+
if (common < child.edge.length) {
|
|
32
|
+
const tail = { edge: child.edge.slice(common), end: child.end, children: child.children };
|
|
33
|
+
child.edge = child.edge.slice(0, common);
|
|
34
|
+
child.end = false;
|
|
35
|
+
child.children = new Map([[tail.edge.charCodeAt(0), tail]]);
|
|
36
|
+
}
|
|
37
|
+
node = child;
|
|
38
|
+
rest = rest.slice(common);
|
|
39
|
+
}
|
|
40
|
+
if (node !== root)
|
|
41
|
+
node.end = true;
|
|
42
|
+
}
|
|
43
|
+
return root;
|
|
44
|
+
};
|
|
45
|
+
const jsonEscaped = (value) => JSON.stringify(value).slice(1, -1);
|
|
46
|
+
// A written line holds each secret JSON-escaped, and `encode` may have turned LF into CR before serialization.
|
|
47
|
+
const lineForms = (mask) => [jsonEscaped(mask), jsonEscaped(mask.replaceAll('\n', '\r'))];
|
|
48
|
+
const indexes = new WeakMap();
|
|
49
|
+
const indexOf = (masks) => {
|
|
50
|
+
const cached = indexes.get(masks);
|
|
51
|
+
if (cached?.version === masks.version)
|
|
52
|
+
return cached;
|
|
53
|
+
const line = indexMasks(new Set([...masks].flatMap(lineForms)));
|
|
54
|
+
const built = { version: masks.version, plain: indexMasks(masks), line };
|
|
55
|
+
indexes.set(masks, built);
|
|
56
|
+
return built;
|
|
57
|
+
};
|
|
58
|
+
const indexesInReach = (mode) => {
|
|
59
|
+
const scope = (0, storage_1.getRedactionMasks)();
|
|
60
|
+
const all = scope ? [storage_1.processRedactionMasks, scope] : [storage_1.processRedactionMasks];
|
|
61
|
+
return all.filter((masks) => masks.size > 0).map((masks) => indexOf(masks)[mode]);
|
|
62
|
+
};
|
|
63
|
+
const matchLength = (reach, value, cursor) => {
|
|
64
|
+
let longest = 0;
|
|
65
|
+
for (const root of reach) {
|
|
66
|
+
let position = cursor;
|
|
67
|
+
let node = root.children.get(value.charCodeAt(position));
|
|
68
|
+
while (node && value.startsWith(node.edge, position)) {
|
|
69
|
+
position += node.edge.length;
|
|
70
|
+
if (node.end && position - cursor > longest)
|
|
71
|
+
longest = position - cursor;
|
|
72
|
+
node = node.children.get(value.charCodeAt(position));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return longest;
|
|
76
|
+
};
|
|
77
|
+
// In a JSON line only string values are replaced, so property names, numbers, and delimiters keep the line
|
|
78
|
+
// parseable, and the scan steps over whole escape sequences so a replacement never starts inside one.
|
|
79
|
+
const replaceMasks = (value, reach, jsonLine) => {
|
|
80
|
+
if (reach.length === 0)
|
|
81
|
+
return value;
|
|
82
|
+
let redacted = '';
|
|
83
|
+
let copied = 0;
|
|
84
|
+
let cursor = 0;
|
|
85
|
+
let inString = !jsonLine;
|
|
86
|
+
let masking = !jsonLine;
|
|
87
|
+
const containers = [];
|
|
88
|
+
let expectKey = false;
|
|
89
|
+
while (cursor < value.length) {
|
|
90
|
+
const unit = value[cursor];
|
|
91
|
+
if (jsonLine && !inString) {
|
|
92
|
+
if (unit === '"') {
|
|
93
|
+
inString = true;
|
|
94
|
+
masking = !expectKey;
|
|
95
|
+
}
|
|
96
|
+
else if (unit === '{' || unit === '[') {
|
|
97
|
+
containers.push(unit);
|
|
98
|
+
expectKey = unit === '{';
|
|
99
|
+
}
|
|
100
|
+
else if (unit === '}' || unit === ']') {
|
|
101
|
+
containers.pop();
|
|
102
|
+
}
|
|
103
|
+
else if (unit === ',') {
|
|
104
|
+
expectKey = containers.at(-1) === '{';
|
|
105
|
+
}
|
|
106
|
+
else if (unit === ':') {
|
|
107
|
+
expectKey = false;
|
|
108
|
+
}
|
|
109
|
+
cursor += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (jsonLine && unit === '"') {
|
|
113
|
+
inString = false;
|
|
114
|
+
cursor += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const length = masking ? matchLength(reach, value, cursor) : 0;
|
|
118
|
+
if (length > 0) {
|
|
119
|
+
redacted += value.slice(copied, cursor) + REDACTED;
|
|
120
|
+
cursor += length;
|
|
121
|
+
copied = cursor;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
cursor += jsonLine && unit === '\\' ? (value[cursor + 1] === 'u' ? 6 : 2) : 1;
|
|
125
|
+
}
|
|
126
|
+
return copied === 0 ? value : redacted + value.slice(copied);
|
|
127
|
+
};
|
|
128
|
+
function applyRedactionMasks(value) {
|
|
129
|
+
return replaceMasks(value, indexesInReach('plain'), false);
|
|
130
|
+
}
|
|
131
|
+
// Masks the finished JSON line, so a mask registered after a child froze its bindings, or a value a serializer or
|
|
132
|
+
// formatter produced, is still caught.
|
|
133
|
+
function redactLogLine(line) {
|
|
134
|
+
return replaceMasks(line, indexesInReach('line'), true);
|
|
135
|
+
}
|
package/dist/span.d.ts
CHANGED
|
@@ -2,10 +2,8 @@ import type { Maybe } from './types';
|
|
|
2
2
|
export type LogPayloadGetterOptions<T> = Maybe<T>;
|
|
3
3
|
type LogPayloadGetter<T> = object | ((o?: LogPayloadGetterOptions<T>) => object);
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @param callback - The async callback to execute
|
|
8
|
-
* @returns The result of the callback
|
|
5
|
+
* Logs `<spanName> started/completed/failed` with duration (ms); nested spans chain spanId
|
|
6
|
+
* "child parent". getter receives {result} or {error}.
|
|
9
7
|
*/
|
|
10
8
|
export declare const startSpan: <T>(spanName: string, callback: () => Promise<T>, getter?: LogPayloadGetter<T>) => Promise<T>;
|
|
11
9
|
export {};
|
package/dist/span.js
CHANGED
|
@@ -6,17 +6,14 @@ const logger_1 = require("./logger");
|
|
|
6
6
|
const storage_1 = require("./storage");
|
|
7
7
|
const trace_1 = require("./trace");
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param callback - The async callback to execute
|
|
12
|
-
* @returns The result of the callback
|
|
9
|
+
* Logs `<spanName> started/completed/failed` with duration (ms); nested spans chain spanId
|
|
10
|
+
* "child parent". getter receives {result} or {error}.
|
|
13
11
|
*/
|
|
14
12
|
const startSpan = async (spanName, callback, getter) => {
|
|
15
13
|
const spanId = (0, uuid_1.v4)();
|
|
16
14
|
const start = performance.now();
|
|
17
15
|
const previous = storage_1.storage.getStore();
|
|
18
|
-
return storage_1.
|
|
19
|
-
...previous,
|
|
16
|
+
return (0, storage_1.runWithStorage)({
|
|
20
17
|
traceId: previous?.traceId ?? (0, trace_1.getTraceId)(),
|
|
21
18
|
spanId: previous?.spanId ? `${spanId} ${previous?.spanId}` : spanId,
|
|
22
19
|
}, async () => {
|
|
@@ -34,7 +31,7 @@ const startSpan = async (spanName, callback, getter) => {
|
|
|
34
31
|
};
|
|
35
32
|
exports.startSpan = startSpan;
|
|
36
33
|
const getLogPayload = (getter, o) => {
|
|
37
|
-
if (
|
|
34
|
+
if (!(getter instanceof Function))
|
|
38
35
|
return getter;
|
|
39
36
|
return getter(o);
|
|
40
37
|
};
|
package/dist/storage.d.ts
CHANGED
|
@@ -4,9 +4,25 @@ export interface UnnboundContext {
|
|
|
4
4
|
spanId?: string;
|
|
5
5
|
messageId?: string;
|
|
6
6
|
}
|
|
7
|
+
/** Process-wide masks are bounded by count: the least recently registered secret is evicted first. */
|
|
8
|
+
export declare const PROCESS_REDACTION_MASK_LIMIT = 4096;
|
|
9
|
+
export declare class MaskSet extends Set<string> {
|
|
10
|
+
version: number;
|
|
11
|
+
add(mask: string): this;
|
|
12
|
+
delete(mask: string): boolean;
|
|
13
|
+
clear(): void;
|
|
14
|
+
}
|
|
15
|
+
declare class RecentMaskSet extends MaskSet {
|
|
16
|
+
private readonly limit;
|
|
17
|
+
constructor(limit: number);
|
|
18
|
+
add(mask: string): this;
|
|
19
|
+
}
|
|
20
|
+
export declare const processRedactionMasks: RecentMaskSet;
|
|
7
21
|
interface UnnboundStorage extends AsyncLocalStorage<object> {
|
|
8
22
|
getStore<T>(): T | undefined;
|
|
9
23
|
}
|
|
10
24
|
export declare const storage: UnnboundStorage;
|
|
11
|
-
export declare const
|
|
25
|
+
export declare const getRedactionMasks: () => MaskSet | undefined;
|
|
26
|
+
export declare const runWithStorage: <T, Data extends object>(data: Data, callback: () => T) => T;
|
|
27
|
+
export declare const withStorage: <T, Data extends object>(data: Data, callback: () => Promise<T>) => Promise<T>;
|
|
12
28
|
export {};
|
package/dist/storage.js
CHANGED
|
@@ -1,10 +1,100 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.withStorage = exports.storage = void 0;
|
|
3
|
+
exports.withStorage = exports.runWithStorage = exports.getRedactionMasks = exports.storage = exports.processRedactionMasks = exports.MaskSet = exports.PROCESS_REDACTION_MASK_LIMIT = void 0;
|
|
4
4
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const redactionMasksKey = Symbol('redactionMasks');
|
|
6
|
+
/** Process-wide masks are bounded by count: the least recently registered secret is evicted first. */
|
|
7
|
+
exports.PROCESS_REDACTION_MASK_LIMIT = 4096;
|
|
8
|
+
// `version` changes only when membership does, so redaction rebuilds its mask index for a new or removed mask,
|
|
9
|
+
// not when a re-registration moves an existing one to the newest position.
|
|
10
|
+
class MaskSet extends Set {
|
|
11
|
+
version = 0;
|
|
12
|
+
add(mask) {
|
|
13
|
+
const present = Set.prototype.delete.call(this, mask);
|
|
14
|
+
Set.prototype.add.call(this, mask);
|
|
15
|
+
if (!present)
|
|
16
|
+
this.version += 1;
|
|
17
|
+
return this;
|
|
18
|
+
}
|
|
19
|
+
delete(mask) {
|
|
20
|
+
const removed = super.delete(mask);
|
|
21
|
+
if (removed)
|
|
22
|
+
this.version += 1;
|
|
23
|
+
return removed;
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
this.version += 1;
|
|
27
|
+
super.clear();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.MaskSet = MaskSet;
|
|
31
|
+
// Every resolver read re-registers its secrets, so one still being resolved stays resident while cold ones age out.
|
|
32
|
+
class RecentMaskSet extends MaskSet {
|
|
33
|
+
limit;
|
|
34
|
+
constructor(limit) {
|
|
35
|
+
super();
|
|
36
|
+
this.limit = limit;
|
|
37
|
+
}
|
|
38
|
+
add(mask) {
|
|
39
|
+
super.add(mask);
|
|
40
|
+
if (this.size > this.limit) {
|
|
41
|
+
const oldest = this.values().next();
|
|
42
|
+
if (!oldest.done)
|
|
43
|
+
this.delete(oldest.value);
|
|
44
|
+
}
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.processRedactionMasks = new RecentMaskSet(exports.PROCESS_REDACTION_MASK_LIMIT);
|
|
49
|
+
// A root scope retains its masks once; detached work (a timer, an unawaited promise) that registers after that
|
|
50
|
+
// point forwards to the process set for logs outside the scope, and keeps its own copy, which the cap cannot evict.
|
|
51
|
+
class ScopeMasks extends MaskSet {
|
|
52
|
+
retained = false;
|
|
53
|
+
// Re-registration moves the mask to the end, so retention hands the process set the scope's masks by recency.
|
|
54
|
+
// A mask the process set already holds is refreshed there too: another trace may log it while this scope is open.
|
|
55
|
+
add(mask) {
|
|
56
|
+
if (this.retained || exports.processRedactionMasks.has(mask))
|
|
57
|
+
exports.processRedactionMasks.add(mask);
|
|
58
|
+
super.add(mask);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.storage =
|
|
63
|
+
// SAFETY: UnnboundStorage only adds a typed getStore helper over AsyncLocalStorage
|
|
64
|
+
new node_async_hooks_1.AsyncLocalStorage();
|
|
65
|
+
const getRedactionMasks = () => exports.storage.getStore()?.[redactionMasksKey];
|
|
66
|
+
exports.getRedactionMasks = getRedactionMasks;
|
|
67
|
+
const retainMasks = (masks) => {
|
|
68
|
+
masks.retained = true;
|
|
69
|
+
for (const mask of masks)
|
|
70
|
+
exports.processRedactionMasks.add(mask);
|
|
71
|
+
};
|
|
72
|
+
// Nested scopes share the root's mask set; a root scope's secrets are returned and logged after it
|
|
73
|
+
// exits, so its masks move to the process set on completion.
|
|
74
|
+
const runWithStorage = (data, callback) => {
|
|
7
75
|
const previous = exports.storage.getStore();
|
|
8
|
-
|
|
76
|
+
const inherited = previous?.[redactionMasksKey];
|
|
77
|
+
const masks = inherited ?? new ScopeMasks();
|
|
78
|
+
const context = { ...previous, ...data, [redactionMasksKey]: masks };
|
|
79
|
+
if (inherited)
|
|
80
|
+
return exports.storage.run(context, callback);
|
|
81
|
+
let result;
|
|
82
|
+
try {
|
|
83
|
+
result = exports.storage.run(context, callback);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
retainMasks(masks);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
if (result instanceof Promise) {
|
|
90
|
+
result.finally(() => retainMasks(masks)).catch(() => undefined);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
retainMasks(masks);
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
exports.runWithStorage = runWithStorage;
|
|
97
|
+
const withStorage = async (data, callback) => {
|
|
98
|
+
return (0, exports.runWithStorage)(data, callback);
|
|
9
99
|
};
|
|
10
100
|
exports.withStorage = withStorage;
|
package/dist/trace.d.ts
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generates a trace ID
|
|
3
|
-
* @returns A trace ID
|
|
4
|
-
*/
|
|
5
1
|
export declare const getTraceId: () => string;
|
|
6
|
-
/**
|
|
7
|
-
* Generates a message ID
|
|
8
|
-
* @public - used by SDK consumers via dist/trace
|
|
9
|
-
* @returns A message ID
|
|
10
|
-
*/
|
|
2
|
+
/** @public Consumed via dist/trace; keep exported. */
|
|
11
3
|
export declare const getMessageId: () => string;
|
|
12
|
-
/**
|
|
13
|
-
* Runs a callback with a trace ID
|
|
14
|
-
* @param callback - The callback to run
|
|
15
|
-
* @param extra - Extra context to add to the trace
|
|
16
|
-
* @returns The result of the callback
|
|
17
|
-
*/
|
|
4
|
+
/** traceId/messageId precedence: extra → enclosing store → fresh uuid. */
|
|
18
5
|
export declare const withTrace: <T, E extends {
|
|
19
6
|
traceId?: string;
|
|
20
7
|
messageId?: string;
|
package/dist/trace.js
CHANGED
|
@@ -3,29 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.withTrace = exports.getMessageId = exports.getTraceId = void 0;
|
|
4
4
|
const uuid_1 = require("uuid");
|
|
5
5
|
const storage_1 = require("./storage");
|
|
6
|
-
/**
|
|
7
|
-
* Generates a trace ID
|
|
8
|
-
* @returns A trace ID
|
|
9
|
-
*/
|
|
10
6
|
const getTraceId = () => (0, uuid_1.v4)();
|
|
11
7
|
exports.getTraceId = getTraceId;
|
|
12
|
-
/**
|
|
13
|
-
* Generates a message ID
|
|
14
|
-
* @public - used by SDK consumers via dist/trace
|
|
15
|
-
* @returns A message ID
|
|
16
|
-
*/
|
|
8
|
+
/** @public Consumed via dist/trace; keep exported. */
|
|
17
9
|
const getMessageId = () => (0, uuid_1.v4)();
|
|
18
10
|
exports.getMessageId = getMessageId;
|
|
19
|
-
/**
|
|
20
|
-
* Runs a callback with a trace ID
|
|
21
|
-
* @param callback - The callback to run
|
|
22
|
-
* @param extra - Extra context to add to the trace
|
|
23
|
-
* @returns The result of the callback
|
|
24
|
-
*/
|
|
11
|
+
/** traceId/messageId precedence: extra → enclosing store → fresh uuid. */
|
|
25
12
|
const withTrace = (callback, extra) => {
|
|
26
13
|
const previous = storage_1.storage.getStore();
|
|
27
14
|
const traceId = extra?.traceId ?? previous?.traceId ?? (0, exports.getTraceId)();
|
|
28
15
|
const messageId = extra?.messageId ?? previous?.messageId ?? (0, exports.getMessageId)();
|
|
29
|
-
return storage_1.
|
|
16
|
+
return (0, storage_1.runWithStorage)({ ...extra, traceId, messageId }, callback);
|
|
30
17
|
};
|
|
31
18
|
exports.withTrace = withTrace;
|
package/dist/types.d.ts
CHANGED
|
@@ -51,26 +51,19 @@ export interface SftpPayload {
|
|
|
51
51
|
files?: string[];
|
|
52
52
|
content?: string;
|
|
53
53
|
exists?: string | false;
|
|
54
|
+
mode?: number | string;
|
|
54
55
|
}
|
|
55
56
|
export type EdiX12Operation = 'fromX12' | 'toX12' | 'validateX12' | 'acknowledgeX12';
|
|
56
|
-
export type
|
|
57
|
-
export type
|
|
58
|
-
export
|
|
59
|
-
export interface EdiTransactionPayload {
|
|
57
|
+
export type EdiOperation = EdiX12Operation;
|
|
58
|
+
export type EdiFormat = 'x12';
|
|
59
|
+
export interface EdiX12Payload {
|
|
60
60
|
input: unknown;
|
|
61
61
|
output: unknown;
|
|
62
62
|
}
|
|
63
|
-
/** @deprecated Use EdiTransactionPayload — the shape is identical across formats. */
|
|
64
|
-
export type EdiX12Payload = EdiTransactionPayload;
|
|
65
|
-
/** @deprecated Use EdiTransactionPayload — the shape is identical across formats. */
|
|
66
|
-
export type EdiEdifactPayload = EdiTransactionPayload;
|
|
67
63
|
export interface EdiPayload {
|
|
68
64
|
operation: EdiOperation;
|
|
69
65
|
type: EdiFormat;
|
|
70
|
-
|
|
71
|
-
transaction?: EdiTransactionPayload;
|
|
72
|
-
/** Legacy key written by pre-EDIFACT SDK versions — read-only. */
|
|
73
|
-
x12?: EdiTransactionPayload;
|
|
66
|
+
x12?: EdiX12Payload;
|
|
74
67
|
}
|
|
75
68
|
export type As2Operation = 'send' | 'deliver' | 'receive' | 'acknowledge' | 'mdn-generate' | 'mdn-deliver' | 'mdn-verify';
|
|
76
69
|
export type As2Outcome = 'confirmed' | 'failed' | 'pending' | 'received' | 'duplicate' | 'inFlight' | 'parked' | 'delivered' | 'generated' | 'verified';
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checks if a path matches any of the ignore patterns
|
|
3
|
-
* @param path - The path to check
|
|
4
|
-
* @param patterns - Array of glob patterns to match against
|
|
5
|
-
* @returns boolean indicating if the path should be ignored
|
|
6
|
-
*/
|
|
1
|
+
/** patterns are globs (`*`, `?`) matched against the whole path; any other metacharacter is literal. */
|
|
7
2
|
export declare const shouldIgnorePath: (path: string, patterns: string[]) => boolean;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Normalizes IP addresses by removing IPv6 mapping prefix for IPv4 addresses.
|
|
16
|
-
* @param ip The IP address to normalize.
|
|
17
|
-
* @returns Normalized IP address string.
|
|
18
|
-
*/
|
|
3
|
+
type ParsedJson = string | number | boolean | null | ParsedJson[] | {
|
|
4
|
+
[key: string]: ParsedJson;
|
|
5
|
+
};
|
|
6
|
+
/** Non-string or invalid JSON → input returned unchanged. */
|
|
7
|
+
export declare function safeJsonParse<Body>(data: Body): Body | ParsedJson;
|
|
8
|
+
/** Strips the IPv4-mapped IPv6 prefix (::ffff:). */
|
|
19
9
|
export declare function normalizeIp(ip: string | undefined): string | undefined;
|
|
10
|
+
export {};
|
package/dist/utils.js
CHANGED
|
@@ -3,29 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.shouldIgnorePath = void 0;
|
|
4
4
|
exports.safeJsonParse = safeJsonParse;
|
|
5
5
|
exports.normalizeIp = normalizeIp;
|
|
6
|
-
/**
|
|
7
|
-
* Checks if a path matches any of the ignore patterns
|
|
8
|
-
* @param path - The path to check
|
|
9
|
-
* @param patterns - Array of glob patterns to match against
|
|
10
|
-
* @returns boolean indicating if the path should be ignored
|
|
11
|
-
*/
|
|
6
|
+
/** patterns are globs (`*`, `?`) matched against the whole path; any other metacharacter is literal. */
|
|
12
7
|
const shouldIgnorePath = (path, patterns) => {
|
|
13
8
|
return patterns.some((pattern) => {
|
|
14
|
-
|
|
15
|
-
const regexPattern = pattern
|
|
16
|
-
.replace(/\./g, '\\.') // Escape dots
|
|
17
|
-
.replace(/\*/g, '.*') // Convert * to .*
|
|
18
|
-
.replace(/\?/g, '.'); // Convert ? to .
|
|
9
|
+
const regexPattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, (character) => character === '*' ? '.*' : character === '?' ? '.' : `\\${character}`);
|
|
19
10
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
20
11
|
return regex.test(path);
|
|
21
12
|
});
|
|
22
13
|
};
|
|
23
14
|
exports.shouldIgnorePath = shouldIgnorePath;
|
|
24
|
-
/**
|
|
25
|
-
* Safely parses JSON strings, returning the original data if parsing fails or if it's not a string.
|
|
26
|
-
* @param data The data to potentially parse as JSON.
|
|
27
|
-
* @returns Parsed JSON object or the original data.
|
|
28
|
-
*/
|
|
15
|
+
/** Non-string or invalid JSON → input returned unchanged. */
|
|
29
16
|
function safeJsonParse(data) {
|
|
30
17
|
if (typeof data === 'string') {
|
|
31
18
|
try {
|
|
@@ -37,15 +24,10 @@ function safeJsonParse(data) {
|
|
|
37
24
|
}
|
|
38
25
|
return data;
|
|
39
26
|
}
|
|
40
|
-
/**
|
|
41
|
-
* Normalizes IP addresses by removing IPv6 mapping prefix for IPv4 addresses.
|
|
42
|
-
* @param ip The IP address to normalize.
|
|
43
|
-
* @returns Normalized IP address string.
|
|
44
|
-
*/
|
|
27
|
+
/** Strips the IPv4-mapped IPv6 prefix (::ffff:). */
|
|
45
28
|
function normalizeIp(ip) {
|
|
46
29
|
if (!ip)
|
|
47
30
|
return ip;
|
|
48
|
-
// Remove IPv4-mapped IPv6 prefix (::ffff:) to get clean IPv4 address
|
|
49
31
|
if (ip.startsWith('::ffff:')) {
|
|
50
32
|
return ip.substring(7);
|
|
51
33
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unnbound-logger-sdk",
|
|
3
3
|
"description": "A structured logging library with TypeScript support using Pino. Provides consistent, well-typed logging with automatic logId, workflowId, traceId, and deploymentId tracking across operational contexts.",
|
|
4
|
-
"version": "3.2.
|
|
4
|
+
"version": "3.2.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"keywords": [
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"url": "https://github.com/unnbounddev/unnbound-sdks/issues"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"axios": "1.
|
|
28
|
+
"axios": "1.18.0",
|
|
29
29
|
"express": "^4.0.0 || ^5.0.0",
|
|
30
30
|
"pino": "^10.3.1",
|
|
31
31
|
"uuid": "^11.1.1"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"vitest": "^4.0.15"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"axios": "^1.
|
|
40
|
+
"axios": "^1.18.0",
|
|
41
41
|
"express": "^4.0.0 || ^5.0.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"build": "tsc",
|
|
62
62
|
"test": "vitest run src",
|
|
63
63
|
"typecheck": "tsc --noEmit",
|
|
64
|
-
"format": "
|
|
65
|
-
"format:check": "
|
|
64
|
+
"format": "oxfmt --write .",
|
|
65
|
+
"format:check": "oxfmt --check .",
|
|
66
66
|
"start:example": "tsx watch examples/node-express.ts",
|
|
67
67
|
"version:bump": "npm version patch"
|
|
68
68
|
}
|