@alchemy/common 0.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/esm/actions/addBreadCrumb.d.ts +14 -0
- package/dist/esm/actions/addBreadCrumb.js +27 -0
- package/dist/esm/actions/addBreadCrumb.js.map +1 -0
- package/dist/esm/chains.d.ts +234 -0
- package/dist/esm/chains.js +113 -0
- package/dist/esm/chains.js.map +1 -0
- package/dist/esm/errors/AccountNotFoundError.d.ts +10 -0
- package/dist/esm/errors/AccountNotFoundError.js +19 -0
- package/dist/esm/errors/AccountNotFoundError.js.map +1 -0
- package/dist/esm/errors/BaseError.d.ts +23 -0
- package/dist/esm/errors/BaseError.js +40 -0
- package/dist/esm/errors/BaseError.js.map +1 -0
- package/dist/esm/errors/ChainNotFoundError.d.ts +11 -0
- package/dist/esm/errors/ChainNotFoundError.js +19 -0
- package/dist/esm/errors/ChainNotFoundError.js.map +1 -0
- package/dist/esm/errors/ConnectionConfigError.d.ts +13 -0
- package/dist/esm/errors/ConnectionConfigError.js +25 -0
- package/dist/esm/errors/ConnectionConfigError.js.map +1 -0
- package/dist/esm/errors/FetchError.d.ts +15 -0
- package/dist/esm/errors/FetchError.js +25 -0
- package/dist/esm/errors/FetchError.js.map +1 -0
- package/dist/esm/errors/InvalidRequestError.d.ts +13 -0
- package/dist/esm/errors/InvalidRequestError.js +22 -0
- package/dist/esm/errors/InvalidRequestError.js.map +1 -0
- package/dist/esm/errors/MethodUnsupportedError.d.ts +13 -0
- package/dist/esm/errors/MethodUnsupportedError.js +21 -0
- package/dist/esm/errors/MethodUnsupportedError.js.map +1 -0
- package/dist/esm/errors/ServerError.d.ts +15 -0
- package/dist/esm/errors/ServerError.js +25 -0
- package/dist/esm/errors/ServerError.js.map +1 -0
- package/dist/esm/index.d.ts +29 -0
- package/dist/esm/index.js +27 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/logging/config.d.ts +190 -0
- package/dist/esm/logging/config.js +279 -0
- package/dist/esm/logging/config.js.map +1 -0
- package/dist/esm/logging/index.d.ts +6 -0
- package/dist/esm/logging/index.js +5 -0
- package/dist/esm/logging/index.js.map +1 -0
- package/dist/esm/logging/local.d.ts +10 -0
- package/dist/esm/logging/local.js +35 -0
- package/dist/esm/logging/local.js.map +1 -0
- package/dist/esm/logging/logger.d.ts +80 -0
- package/dist/esm/logging/logger.js +111 -0
- package/dist/esm/logging/logger.js.map +1 -0
- package/dist/esm/logging/noop.d.ts +6 -0
- package/dist/esm/logging/noop.js +12 -0
- package/dist/esm/logging/noop.js.map +1 -0
- package/dist/esm/logging/sinks.d.ts +90 -0
- package/dist/esm/logging/sinks.js +111 -0
- package/dist/esm/logging/sinks.js.map +1 -0
- package/dist/esm/logging/types.d.ts +96 -0
- package/dist/esm/logging/types.js +2 -0
- package/dist/esm/logging/types.js.map +1 -0
- package/dist/esm/logging/utils.d.ts +7 -0
- package/dist/esm/logging/utils.js +21 -0
- package/dist/esm/logging/utils.js.map +1 -0
- package/dist/esm/rest/restClient.d.ts +34 -0
- package/dist/esm/rest/restClient.js +55 -0
- package/dist/esm/rest/restClient.js.map +1 -0
- package/dist/esm/rest/types.d.ts +24 -0
- package/dist/esm/rest/types.js +2 -0
- package/dist/esm/rest/types.js.map +1 -0
- package/dist/esm/tracing/traceHeader.d.ts +82 -0
- package/dist/esm/tracing/traceHeader.js +145 -0
- package/dist/esm/tracing/traceHeader.js.map +1 -0
- package/dist/esm/tracing/updateHeaders.d.ts +24 -0
- package/dist/esm/tracing/updateHeaders.js +61 -0
- package/dist/esm/tracing/updateHeaders.js.map +1 -0
- package/dist/esm/transport/alchemy.d.ts +110 -0
- package/dist/esm/transport/alchemy.js +164 -0
- package/dist/esm/transport/alchemy.js.map +1 -0
- package/dist/esm/transport/chainRegistry.d.ts +31 -0
- package/dist/esm/transport/chainRegistry.js +95 -0
- package/dist/esm/transport/chainRegistry.js.map +1 -0
- package/dist/esm/transport/connection.d.ts +20 -0
- package/dist/esm/transport/connection.js +2 -0
- package/dist/esm/transport/connection.js.map +1 -0
- package/dist/esm/transport/connectionSchema.d.ts +124 -0
- package/dist/esm/transport/connectionSchema.js +121 -0
- package/dist/esm/transport/connectionSchema.js.map +1 -0
- package/dist/esm/utils/assertNever.d.ts +8 -0
- package/dist/esm/utils/assertNever.js +12 -0
- package/dist/esm/utils/assertNever.js.map +1 -0
- package/dist/esm/utils/bigint.d.ts +24 -0
- package/dist/esm/utils/bigint.js +37 -0
- package/dist/esm/utils/bigint.js.map +1 -0
- package/dist/esm/utils/createEip1193HandlerFactory.d.ts +18 -0
- package/dist/esm/utils/createEip1193HandlerFactory.js +11 -0
- package/dist/esm/utils/createEip1193HandlerFactory.js.map +1 -0
- package/dist/esm/utils/headers.d.ts +7 -0
- package/dist/esm/utils/headers.js +29 -0
- package/dist/esm/utils/headers.js.map +1 -0
- package/dist/esm/utils/lowerAddress.d.ts +8 -0
- package/dist/esm/utils/lowerAddress.js +9 -0
- package/dist/esm/utils/lowerAddress.js.map +1 -0
- package/dist/esm/utils/raise.d.ts +8 -0
- package/dist/esm/utils/raise.js +14 -0
- package/dist/esm/utils/raise.js.map +1 -0
- package/dist/esm/utils/types.d.ts +10 -0
- package/dist/esm/utils/types.js +2 -0
- package/dist/esm/utils/types.js.map +1 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +4 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/types/actions/addBreadCrumb.d.ts +15 -0
- package/dist/types/actions/addBreadCrumb.d.ts.map +1 -0
- package/dist/types/chains.d.ts +235 -0
- package/dist/types/chains.d.ts.map +1 -0
- package/dist/types/errors/AccountNotFoundError.d.ts +11 -0
- package/dist/types/errors/AccountNotFoundError.d.ts.map +1 -0
- package/dist/types/errors/BaseError.d.ts +24 -0
- package/dist/types/errors/BaseError.d.ts.map +1 -0
- package/dist/types/errors/ChainNotFoundError.d.ts +12 -0
- package/dist/types/errors/ChainNotFoundError.d.ts.map +1 -0
- package/dist/types/errors/ConnectionConfigError.d.ts +14 -0
- package/dist/types/errors/ConnectionConfigError.d.ts.map +1 -0
- package/dist/types/errors/FetchError.d.ts +16 -0
- package/dist/types/errors/FetchError.d.ts.map +1 -0
- package/dist/types/errors/InvalidRequestError.d.ts +14 -0
- package/dist/types/errors/InvalidRequestError.d.ts.map +1 -0
- package/dist/types/errors/MethodUnsupportedError.d.ts +14 -0
- package/dist/types/errors/MethodUnsupportedError.d.ts.map +1 -0
- package/dist/types/errors/ServerError.d.ts +16 -0
- package/dist/types/errors/ServerError.d.ts.map +1 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/logging/config.d.ts +191 -0
- package/dist/types/logging/config.d.ts.map +1 -0
- package/dist/types/logging/index.d.ts +7 -0
- package/dist/types/logging/index.d.ts.map +1 -0
- package/dist/types/logging/local.d.ts +11 -0
- package/dist/types/logging/local.d.ts.map +1 -0
- package/dist/types/logging/logger.d.ts +81 -0
- package/dist/types/logging/logger.d.ts.map +1 -0
- package/dist/types/logging/noop.d.ts +7 -0
- package/dist/types/logging/noop.d.ts.map +1 -0
- package/dist/types/logging/sinks.d.ts +91 -0
- package/dist/types/logging/sinks.d.ts.map +1 -0
- package/dist/types/logging/types.d.ts +97 -0
- package/dist/types/logging/types.d.ts.map +1 -0
- package/dist/types/logging/utils.d.ts +8 -0
- package/dist/types/logging/utils.d.ts.map +1 -0
- package/dist/types/rest/restClient.d.ts +35 -0
- package/dist/types/rest/restClient.d.ts.map +1 -0
- package/dist/types/rest/types.d.ts +25 -0
- package/dist/types/rest/types.d.ts.map +1 -0
- package/dist/types/tracing/traceHeader.d.ts +83 -0
- package/dist/types/tracing/traceHeader.d.ts.map +1 -0
- package/dist/types/tracing/updateHeaders.d.ts +25 -0
- package/dist/types/tracing/updateHeaders.d.ts.map +1 -0
- package/dist/types/transport/alchemy.d.ts +111 -0
- package/dist/types/transport/alchemy.d.ts.map +1 -0
- package/dist/types/transport/chainRegistry.d.ts +32 -0
- package/dist/types/transport/chainRegistry.d.ts.map +1 -0
- package/dist/types/transport/connection.d.ts +21 -0
- package/dist/types/transport/connection.d.ts.map +1 -0
- package/dist/types/transport/connectionSchema.d.ts +125 -0
- package/dist/types/transport/connectionSchema.d.ts.map +1 -0
- package/dist/types/utils/assertNever.d.ts +9 -0
- package/dist/types/utils/assertNever.d.ts.map +1 -0
- package/dist/types/utils/bigint.d.ts +25 -0
- package/dist/types/utils/bigint.d.ts.map +1 -0
- package/dist/types/utils/createEip1193HandlerFactory.d.ts +19 -0
- package/dist/types/utils/createEip1193HandlerFactory.d.ts.map +1 -0
- package/dist/types/utils/headers.d.ts +8 -0
- package/dist/types/utils/headers.d.ts.map +1 -0
- package/dist/types/utils/lowerAddress.d.ts +9 -0
- package/dist/types/utils/lowerAddress.d.ts.map +1 -0
- package/dist/types/utils/raise.d.ts +9 -0
- package/dist/types/utils/raise.d.ts.map +1 -0
- package/dist/types/utils/types.d.ts +11 -0
- package/dist/types/utils/types.d.ts.map +1 -0
- package/dist/types/version.d.ts +2 -0
- package/dist/types/version.d.ts.map +1 -0
- package/package.json +67 -0
- package/src/actions/addBreadCrumb.ts +38 -0
- package/src/chains.ts +118 -0
- package/src/errors/AccountNotFoundError.ts +16 -0
- package/src/errors/BaseError.ts +51 -0
- package/src/errors/ChainNotFoundError.ts +15 -0
- package/src/errors/ConnectionConfigError.ts +22 -0
- package/src/errors/FetchError.ts +21 -0
- package/src/errors/InvalidRequestError.ts +19 -0
- package/src/errors/MethodUnsupportedError.ts +17 -0
- package/src/errors/ServerError.ts +21 -0
- package/src/index.ts +60 -0
- package/src/logging/config.ts +365 -0
- package/src/logging/index.ts +20 -0
- package/src/logging/local.ts +39 -0
- package/src/logging/logger.ts +194 -0
- package/src/logging/noop.ts +13 -0
- package/src/logging/sinks.ts +115 -0
- package/src/logging/types.ts +111 -0
- package/src/logging/utils.ts +31 -0
- package/src/rest/restClient.ts +64 -0
- package/src/rest/types.ts +42 -0
- package/src/tracing/traceHeader.ts +154 -0
- package/src/tracing/updateHeaders.ts +66 -0
- package/src/transport/alchemy.ts +242 -0
- package/src/transport/chainRegistry.ts +115 -0
- package/src/transport/connection.ts +19 -0
- package/src/transport/connectionSchema.ts +145 -0
- package/src/utils/assertNever.ts +12 -0
- package/src/utils/bigint.ts +58 -0
- package/src/utils/createEip1193HandlerFactory.ts +25 -0
- package/src/utils/headers.ts +48 -0
- package/src/utils/lowerAddress.ts +10 -0
- package/src/utils/raise.ts +14 -0
- package/src/utils/types.ts +14 -0
- package/src/version.ts +3 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log level constants for controlling diagnostics output.
|
|
3
|
+
* Lower numeric values indicate higher priority (more restrictive filtering).
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { LogLevel, setGlobalLoggerConfig } from "@alchemy/common";
|
|
8
|
+
*
|
|
9
|
+
* setGlobalLoggerConfig({ level: LogLevel.DEBUG });
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export const LogLevel = {
|
|
13
|
+
/** Critical errors only */
|
|
14
|
+
ERROR: 0,
|
|
15
|
+
/** Warnings and errors */
|
|
16
|
+
WARN: 1,
|
|
17
|
+
/** Informational messages, warnings, and errors */
|
|
18
|
+
INFO: 2,
|
|
19
|
+
/** Debug messages and all above */
|
|
20
|
+
DEBUG: 3,
|
|
21
|
+
/** All messages including verbose details */
|
|
22
|
+
VERBOSE: 4,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
26
|
+
export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for redacting sensitive data in log output.
|
|
30
|
+
*/
|
|
31
|
+
export type RedactConfig = {
|
|
32
|
+
/** Predicate function to test if a key should be redacted. Returns true if the key should be redacted. */
|
|
33
|
+
keys?: (key: string) => boolean;
|
|
34
|
+
/** Function to replace redacted values. Defaults to returning "[REDACTED]". */
|
|
35
|
+
replacer?: (value: unknown, key?: string) => unknown;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Configuration for the diagnostics logging system.
|
|
40
|
+
* All properties are optional and will use sensible defaults if not provided.
|
|
41
|
+
*/
|
|
42
|
+
export type DiagnosticsConfig = {
|
|
43
|
+
/** Minimum log level to emit. Defaults to INFO in development, ERROR in production. */
|
|
44
|
+
level?: LogLevel;
|
|
45
|
+
/** Configuration for redacting sensitive data in logs. */
|
|
46
|
+
redact?: RedactConfig;
|
|
47
|
+
/** Array of sink functions to receive log entries. Defaults to console sink. */
|
|
48
|
+
sinks?: Array<(entry: LogEntry) => void>;
|
|
49
|
+
/** Disable telemetry headers in requests. Defaults to false. */
|
|
50
|
+
disableTelemetryHeaders?: boolean;
|
|
51
|
+
/** Array of exact namespace strings to allow. If undefined or empty, all namespaces are enabled. Example: ["aa-infra", "wallet-apis"] */
|
|
52
|
+
enabledNamespaces?: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Base context attached to all log entries from a logger instance.
|
|
57
|
+
*/
|
|
58
|
+
export type LoggerContextBase = {
|
|
59
|
+
/** Package name (e.g., "@alchemy/aa-infra") */
|
|
60
|
+
package: string;
|
|
61
|
+
/** Package version (e.g., "1.0.0") */
|
|
62
|
+
version: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A single log entry emitted by the diagnostics logger.
|
|
67
|
+
*/
|
|
68
|
+
export type LogEntry = {
|
|
69
|
+
/** Timestamp in milliseconds since epoch (Date.now()) */
|
|
70
|
+
ts: number;
|
|
71
|
+
/** Log level of this entry */
|
|
72
|
+
level: LogLevel;
|
|
73
|
+
/** Optional namespace for filtering/grouping (e.g., "aa-infra", "wallet-apis") */
|
|
74
|
+
namespace: string | undefined;
|
|
75
|
+
/** The log message */
|
|
76
|
+
message: string;
|
|
77
|
+
/** Optional structured data attached to this log entry */
|
|
78
|
+
data?: Record<string, unknown>;
|
|
79
|
+
/** Package context (name and version) */
|
|
80
|
+
context: LoggerContextBase;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let globalConfig: Required<DiagnosticsConfig> | undefined;
|
|
84
|
+
|
|
85
|
+
function defaultConfig(): Required<DiagnosticsConfig> {
|
|
86
|
+
const env = (typeof process !== "undefined" && process.env) || {};
|
|
87
|
+
|
|
88
|
+
// Default level: INFO in dev, ERROR in prod
|
|
89
|
+
const isDev = env.NODE_ENV === "development";
|
|
90
|
+
const defaultLevel = isDev ? LogLevel.INFO : LogLevel.ERROR;
|
|
91
|
+
|
|
92
|
+
let level: LogLevel = defaultLevel;
|
|
93
|
+
const envLevel = env.ALCHEMY_LOG_LEVEL;
|
|
94
|
+
if (envLevel) {
|
|
95
|
+
const normalized = envLevel.toUpperCase() as keyof typeof LogLevel;
|
|
96
|
+
if (normalized in LogLevel) {
|
|
97
|
+
level = LogLevel[normalized];
|
|
98
|
+
} else {
|
|
99
|
+
console.warn(
|
|
100
|
+
`[alchemy/common] Invalid ALCHEMY_LOG_LEVEL value: "${envLevel}". Expected one of: ERROR, WARN, INFO, DEBUG, VERBOSE. Using default: ${isDev ? "INFO" : "ERROR"}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
level,
|
|
107
|
+
redact: {
|
|
108
|
+
keys: (key: string) =>
|
|
109
|
+
/^(authorization|apiKey|jwt|privateKey|secret|password)$/i.test(key),
|
|
110
|
+
replacer: () => "[REDACTED]",
|
|
111
|
+
},
|
|
112
|
+
sinks: [consoleSink],
|
|
113
|
+
disableTelemetryHeaders: false,
|
|
114
|
+
enabledNamespaces: [],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Gets the current global logger configuration.
|
|
120
|
+
* If not yet initialized, returns default configuration based on environment.
|
|
121
|
+
*
|
|
122
|
+
* @returns {Required<DiagnosticsConfig>} The current global configuration with all fields populated
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* import { getGlobalLoggerConfig } from "@alchemy/common";
|
|
126
|
+
*
|
|
127
|
+
* const config = getGlobalLoggerConfig();
|
|
128
|
+
* console.log("Current log level:", config.level);
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function getGlobalLoggerConfig(): Required<DiagnosticsConfig> {
|
|
132
|
+
globalConfig ??= defaultConfig();
|
|
133
|
+
return globalConfig;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sets the global logger configuration.
|
|
138
|
+
* Partial configuration is supported - unspecified fields retain their current values.
|
|
139
|
+
*
|
|
140
|
+
* Configuration precedence (highest to lowest):
|
|
141
|
+
* 1. Explicit setGlobalLoggerConfig calls
|
|
142
|
+
* 2. ALCHEMY_LOG_LEVEL environment variable
|
|
143
|
+
* 3. Defaults (INFO in dev, ERROR in prod)
|
|
144
|
+
*
|
|
145
|
+
* @param {DiagnosticsConfig} cfg - Partial configuration to apply
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { setGlobalLoggerConfig, LogLevel } from "@alchemy/common";
|
|
149
|
+
*
|
|
150
|
+
* // Set log level only
|
|
151
|
+
* setGlobalLoggerConfig({ level: LogLevel.DEBUG });
|
|
152
|
+
*
|
|
153
|
+
* // Add custom sink
|
|
154
|
+
* setGlobalLoggerConfig({
|
|
155
|
+
* sinks: [(entry) => console.log(JSON.stringify(entry))]
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* // Filter to specific namespaces
|
|
159
|
+
* setGlobalLoggerConfig({
|
|
160
|
+
* enabledNamespaces: ["aa-infra", "wallet-apis"]
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function setGlobalLoggerConfig(cfg: DiagnosticsConfig): void {
|
|
165
|
+
const current = getGlobalLoggerConfig();
|
|
166
|
+
globalConfig = {
|
|
167
|
+
level: cfg.level ?? current.level,
|
|
168
|
+
redact: cfg.redact ?? current.redact,
|
|
169
|
+
sinks: cfg.sinks ?? current.sinks,
|
|
170
|
+
disableTelemetryHeaders:
|
|
171
|
+
cfg.disableTelemetryHeaders ?? current.disableTelemetryHeaders,
|
|
172
|
+
enabledNamespaces:
|
|
173
|
+
"enabledNamespaces" in cfg
|
|
174
|
+
? (cfg.enabledNamespaces ?? [])
|
|
175
|
+
: current.enabledNamespaces,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Checks if a given log level is enabled based on current global configuration.
|
|
181
|
+
* Used internally by logger to short-circuit disabled log statements.
|
|
182
|
+
*
|
|
183
|
+
* @param {LogLevel} level - The log level to check
|
|
184
|
+
* @returns {boolean} True if the level is enabled (will be logged), false otherwise
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* import { isLevelEnabled, LogLevel } from "@alchemy/common";
|
|
188
|
+
*
|
|
189
|
+
* if (isLevelEnabled(LogLevel.DEBUG)) {
|
|
190
|
+
* // Perform expensive debug computation
|
|
191
|
+
* }
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export function isLevelEnabled(level: LogLevel): boolean {
|
|
195
|
+
return level <= getGlobalLoggerConfig().level;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Checks if a given namespace is enabled based on current global configuration.
|
|
200
|
+
* Used internally by logger to short-circuit logs from disabled namespaces.
|
|
201
|
+
*
|
|
202
|
+
* @param {string | undefined} namespace - The namespace to check
|
|
203
|
+
* @returns {boolean} True if the namespace is enabled (will be logged), false otherwise
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* import { isNamespaceEnabled } from "@alchemy/common";
|
|
207
|
+
*
|
|
208
|
+
* if (isNamespaceEnabled("aa-infra")) {
|
|
209
|
+
* // This namespace is enabled
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function isNamespaceEnabled(namespace: string | undefined): boolean {
|
|
214
|
+
const { enabledNamespaces } = getGlobalLoggerConfig();
|
|
215
|
+
|
|
216
|
+
// If no filter is set or it's an empty array, all namespaces are enabled
|
|
217
|
+
if (!enabledNamespaces || enabledNamespaces.length === 0) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// If namespace is undefined, disable it (only named namespaces can be filtered)
|
|
222
|
+
if (namespace === undefined) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if the namespace is in the enabled list
|
|
227
|
+
return enabledNamespaces.includes(namespace);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Redacts sensitive keys in an object based on global redaction configuration.
|
|
232
|
+
* Performs deep redaction by recursively processing nested objects and arrays.
|
|
233
|
+
* Default redaction includes: authorization, apiKey, jwt, privateKey, secret, password.
|
|
234
|
+
*
|
|
235
|
+
* @param {Record<string, unknown> | undefined} obj - The object to redact
|
|
236
|
+
* @returns {Record<string, unknown> | undefined} A new object with sensitive values redacted, or undefined if input was undefined
|
|
237
|
+
* @example
|
|
238
|
+
* ```ts
|
|
239
|
+
* import { redactObject } from "@alchemy/common";
|
|
240
|
+
*
|
|
241
|
+
* const data = {
|
|
242
|
+
* apiKey: "secret123",
|
|
243
|
+
* userId: "user-456",
|
|
244
|
+
* nested: { secret: "hidden" }
|
|
245
|
+
* };
|
|
246
|
+
*
|
|
247
|
+
* const redacted = redactObject(data);
|
|
248
|
+
* // { apiKey: "[REDACTED]", userId: "user-456", nested: { secret: "[REDACTED]" } }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export function redactObject(
|
|
252
|
+
obj: Record<string, unknown> | undefined,
|
|
253
|
+
): Record<string, unknown> | undefined {
|
|
254
|
+
if (!obj) return obj;
|
|
255
|
+
const { keys, replacer } = getGlobalLoggerConfig().redact;
|
|
256
|
+
const out: Record<string, unknown> = {};
|
|
257
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
258
|
+
if (keys && keys(k)) {
|
|
259
|
+
out[k] = replacer ? replacer(v, k) : "[REDACTED]";
|
|
260
|
+
} else if (Array.isArray(v)) {
|
|
261
|
+
// Recursively redact array elements
|
|
262
|
+
out[k] = v.map((item) =>
|
|
263
|
+
item != null && typeof item === "object"
|
|
264
|
+
? redactObject(item as Record<string, unknown>)
|
|
265
|
+
: item,
|
|
266
|
+
);
|
|
267
|
+
} else if (v != null && typeof v === "object") {
|
|
268
|
+
// Recursively redact nested objects
|
|
269
|
+
out[k] = redactObject(v as Record<string, unknown>);
|
|
270
|
+
} else {
|
|
271
|
+
out[k] = v;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Format timestamp as HH:MM:SS.mmm
|
|
279
|
+
*
|
|
280
|
+
* @param {number} ts - Timestamp in milliseconds since epoch
|
|
281
|
+
* @returns {string} Formatted timestamp string
|
|
282
|
+
*/
|
|
283
|
+
function formatTimestamp(ts: number): string {
|
|
284
|
+
const date = new Date(ts);
|
|
285
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
286
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
287
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
288
|
+
const milliseconds = String(date.getMilliseconds()).padStart(3, "0");
|
|
289
|
+
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Format data as a JSON object for console output.
|
|
294
|
+
* Filters out context fields (package, version) to avoid duplication.
|
|
295
|
+
* Handles BigInt values by converting them to strings.
|
|
296
|
+
*
|
|
297
|
+
* @param {Record<string, unknown> | undefined} data - The data object to format
|
|
298
|
+
* @returns {string} Formatted JSON string with leading space, or empty string if no data
|
|
299
|
+
*/
|
|
300
|
+
function formatData(data: Record<string, unknown> | undefined): string {
|
|
301
|
+
if (!data || Object.keys(data).length === 0) return "";
|
|
302
|
+
|
|
303
|
+
// Filter out context fields (package, version) and create clean object
|
|
304
|
+
const filtered = Object.entries(data)
|
|
305
|
+
.filter(([key]) => key !== "package" && key !== "version")
|
|
306
|
+
.reduce(
|
|
307
|
+
(acc, [key, value]) => {
|
|
308
|
+
acc[key] = value;
|
|
309
|
+
return acc;
|
|
310
|
+
},
|
|
311
|
+
{} as Record<string, unknown>,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (Object.keys(filtered).length === 0) return "";
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
" " +
|
|
318
|
+
JSON.stringify(filtered, (_key, value) =>
|
|
319
|
+
typeof value === "bigint" ? value.toString() : value,
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Default console sink for diagnostics logging.
|
|
326
|
+
* Routes log entries to appropriate console methods based on log level.
|
|
327
|
+
*
|
|
328
|
+
* Format: [HH:MM:SS.mmm] [package@version] message {data}
|
|
329
|
+
*
|
|
330
|
+
* @param {LogEntry} entry - The log entry to output
|
|
331
|
+
* @example
|
|
332
|
+
* ```ts
|
|
333
|
+
* import { consoleSink, setGlobalLoggerConfig } from "@alchemy/common";
|
|
334
|
+
*
|
|
335
|
+
* // Console sink is used by default, but can be explicitly configured
|
|
336
|
+
* setGlobalLoggerConfig({
|
|
337
|
+
* sinks: [consoleSink]
|
|
338
|
+
* });
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export function consoleSink(entry: LogEntry): void {
|
|
342
|
+
const { ts, level, message, data, context } = entry;
|
|
343
|
+
const timestamp = formatTimestamp(ts);
|
|
344
|
+
const prefix = `[${timestamp}] [${context.package}@${context.version}]`;
|
|
345
|
+
const dataStr = formatData(data);
|
|
346
|
+
const output = `${prefix} ${message}${dataStr}`;
|
|
347
|
+
|
|
348
|
+
switch (level) {
|
|
349
|
+
case LogLevel.ERROR:
|
|
350
|
+
console.error(output);
|
|
351
|
+
break;
|
|
352
|
+
case LogLevel.WARN:
|
|
353
|
+
console.warn(output);
|
|
354
|
+
break;
|
|
355
|
+
case LogLevel.INFO:
|
|
356
|
+
console.info(output);
|
|
357
|
+
break;
|
|
358
|
+
case LogLevel.DEBUG:
|
|
359
|
+
console.debug(output);
|
|
360
|
+
break;
|
|
361
|
+
case LogLevel.VERBOSE:
|
|
362
|
+
console.log(output);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { createLogger, LogLevel, consoleSink } from "./logger.js";
|
|
2
|
+
export type { DiagnosticsLogger } from "./logger.js";
|
|
3
|
+
export {
|
|
4
|
+
setGlobalLoggerConfig,
|
|
5
|
+
getGlobalLoggerConfig,
|
|
6
|
+
isLevelEnabled,
|
|
7
|
+
isNamespaceEnabled,
|
|
8
|
+
redactObject,
|
|
9
|
+
} from "./config.js";
|
|
10
|
+
export type {
|
|
11
|
+
LogEntry,
|
|
12
|
+
DiagnosticsConfig,
|
|
13
|
+
RedactConfig,
|
|
14
|
+
LoggerContextBase,
|
|
15
|
+
} from "./config.js";
|
|
16
|
+
|
|
17
|
+
// Testing utilities
|
|
18
|
+
export { InMemorySink } from "./sinks.js";
|
|
19
|
+
|
|
20
|
+
export type * from "./types.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EventsSchema, InnerLogger, LoggerContext } from "./types.js";
|
|
2
|
+
import { isClientDevMode } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a local-only logger that outputs events to the console in development mode.
|
|
6
|
+
* This logger does not send data to external services and is safe for all environments.
|
|
7
|
+
*
|
|
8
|
+
* @template Schema - The events schema defining allowed events and their data structures
|
|
9
|
+
* @param {LoggerContext} context - Context information to attach to all events
|
|
10
|
+
* @returns {InnerLogger<Schema>} A logger instance that logs to console in dev mode
|
|
11
|
+
*/
|
|
12
|
+
export function createLocalLogger<Schema extends EventsSchema = []>(
|
|
13
|
+
context: LoggerContext,
|
|
14
|
+
): InnerLogger<Schema> {
|
|
15
|
+
const isDev = isClientDevMode();
|
|
16
|
+
|
|
17
|
+
// Generate a simple anonymous ID for local logging
|
|
18
|
+
const anonId = `local-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
_internal: {
|
|
22
|
+
ready: Promise.resolve(),
|
|
23
|
+
anonId,
|
|
24
|
+
},
|
|
25
|
+
trackEvent: async ({ name, data }) => {
|
|
26
|
+
if (isDev) {
|
|
27
|
+
try {
|
|
28
|
+
console.log(`[${context.package}] Event: ${name}`, {
|
|
29
|
+
...data,
|
|
30
|
+
...context,
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
// Silently ignore console logging errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LogLevel,
|
|
3
|
+
consoleSink,
|
|
4
|
+
getGlobalLoggerConfig,
|
|
5
|
+
isLevelEnabled,
|
|
6
|
+
isNamespaceEnabled,
|
|
7
|
+
redactObject,
|
|
8
|
+
type LoggerContextBase,
|
|
9
|
+
type LogEntry,
|
|
10
|
+
} from "./config.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lazy message supplier function for expensive log computations.
|
|
14
|
+
* Only evaluated when the log level is enabled.
|
|
15
|
+
*
|
|
16
|
+
* @returns {[string, Record<string, unknown>?]} Tuple of [message, optional data]
|
|
17
|
+
*/
|
|
18
|
+
type MessageSupplier = () => [string, Record<string, unknown>?];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Diagnostics logger instance for developer-facing debugging output.
|
|
22
|
+
* Provides level-based logging (error, warn, info, debug, verbose) with structured data.
|
|
23
|
+
*/
|
|
24
|
+
export type DiagnosticsLogger = {
|
|
25
|
+
/** Log debug messages for development and troubleshooting */
|
|
26
|
+
debug: (
|
|
27
|
+
msg: string | MessageSupplier,
|
|
28
|
+
data?: Record<string, unknown>,
|
|
29
|
+
) => void;
|
|
30
|
+
/** Log informational messages for key operations */
|
|
31
|
+
info: (msg: string | MessageSupplier, data?: Record<string, unknown>) => void;
|
|
32
|
+
/** Log warnings for recoverable issues */
|
|
33
|
+
warn: (msg: string | MessageSupplier, data?: Record<string, unknown>) => void;
|
|
34
|
+
/** Log errors for failures */
|
|
35
|
+
error: (
|
|
36
|
+
msg: string | MessageSupplier,
|
|
37
|
+
data?: Record<string, unknown>,
|
|
38
|
+
) => void;
|
|
39
|
+
/** Log verbose details for very detailed tracing */
|
|
40
|
+
verbose: (
|
|
41
|
+
msg: string | MessageSupplier,
|
|
42
|
+
data?: Record<string, unknown>,
|
|
43
|
+
) => void;
|
|
44
|
+
/** Create a child logger with additional context merged into all log entries */
|
|
45
|
+
withContext: (extra: Record<string, unknown>) => DiagnosticsLogger;
|
|
46
|
+
/**
|
|
47
|
+
* Wrap a function to measure and log its execution time at DEBUG level.
|
|
48
|
+
* Supports both synchronous and asynchronous functions.
|
|
49
|
+
*
|
|
50
|
+
* @template TArgs - Function argument types
|
|
51
|
+
* @template TRet - Function return type
|
|
52
|
+
* @param name - Name for profiling logs
|
|
53
|
+
* @param func - Function to profile
|
|
54
|
+
* @returns Wrapped function that logs execution time
|
|
55
|
+
*/
|
|
56
|
+
profiled<TArgs extends any[], TRet>(
|
|
57
|
+
name: string,
|
|
58
|
+
func: (...args: TArgs) => TRet,
|
|
59
|
+
): (...args: TArgs) => TRet;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parameters for creating a diagnostics logger instance.
|
|
64
|
+
*/
|
|
65
|
+
export type CreateLoggerParams = LoggerContextBase & {
|
|
66
|
+
/** Optional namespace for filtering/grouping (e.g., "aa-infra", "wallet-apis") */
|
|
67
|
+
namespace?: string;
|
|
68
|
+
/** Optional base context merged into all log entries from this logger */
|
|
69
|
+
baseContext?: Record<string, unknown>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a diagnostics logger instance for a package.
|
|
74
|
+
* Loggers emit structured log entries to configured sinks (default: console).
|
|
75
|
+
*
|
|
76
|
+
* All log methods support:
|
|
77
|
+
* - String messages: `logger.info("message", { data })`
|
|
78
|
+
* - Lazy suppliers: `logger.debug(() => ["expensive", computeData()])` (only evaluated when level enabled)
|
|
79
|
+
*
|
|
80
|
+
* @param {CreateLoggerParams} params - Logger configuration
|
|
81
|
+
* @returns {DiagnosticsLogger} A logger instance
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* import { createLogger } from "@alchemy/common";
|
|
85
|
+
*
|
|
86
|
+
* const logger = createLogger({
|
|
87
|
+
* package: "@alchemy/aa-infra",
|
|
88
|
+
* version: "1.0.0",
|
|
89
|
+
* namespace: "aa-infra"
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* logger.info("processing request", { chainId: 1 });
|
|
93
|
+
* logger.debug("detailed state", { userOp: op });
|
|
94
|
+
*
|
|
95
|
+
* // Child logger with additional context
|
|
96
|
+
* const childLogger = logger.withContext({ requestId: "123" });
|
|
97
|
+
* childLogger.info("step complete"); // includes requestId in all logs
|
|
98
|
+
*
|
|
99
|
+
* // Profile a function
|
|
100
|
+
* const sendWithProfiling = logger.profiled("sendUserOp", sendUserOp);
|
|
101
|
+
* await sendWithProfiling(op); // logs execution time at DEBUG level
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function createLogger(params: CreateLoggerParams): DiagnosticsLogger {
|
|
105
|
+
const { namespace, baseContext, ...ctx } = params;
|
|
106
|
+
|
|
107
|
+
function emit(
|
|
108
|
+
level: LogLevel,
|
|
109
|
+
msgOrFn: string | MessageSupplier,
|
|
110
|
+
data?: Record<string, unknown>,
|
|
111
|
+
) {
|
|
112
|
+
if (!isLevelEnabled(level)) return;
|
|
113
|
+
if (!isNamespaceEnabled(namespace)) return;
|
|
114
|
+
|
|
115
|
+
let message: string;
|
|
116
|
+
let payload: Record<string, unknown> | undefined = data;
|
|
117
|
+
|
|
118
|
+
if (typeof msgOrFn === "function") {
|
|
119
|
+
const [m, d] = msgOrFn();
|
|
120
|
+
message = m;
|
|
121
|
+
payload = d ?? data;
|
|
122
|
+
} else {
|
|
123
|
+
message = msgOrFn;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const entry: LogEntry = {
|
|
127
|
+
ts: Date.now(),
|
|
128
|
+
level,
|
|
129
|
+
namespace,
|
|
130
|
+
message,
|
|
131
|
+
data: redactObject({ ...(baseContext || {}), ...(payload || {}) }),
|
|
132
|
+
context: ctx,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
for (const sink of getGlobalLoggerConfig().sinks) {
|
|
136
|
+
try {
|
|
137
|
+
sink(entry);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
// Silently continue if a sink throws - don't break other sinks
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const logger: DiagnosticsLogger = {
|
|
145
|
+
debug: (m, d) => emit(LogLevel.DEBUG, m, d),
|
|
146
|
+
info: (m, d) => emit(LogLevel.INFO, m, d),
|
|
147
|
+
warn: (m, d) => emit(LogLevel.WARN, m, d),
|
|
148
|
+
error: (m, d) => emit(LogLevel.ERROR, m, d),
|
|
149
|
+
verbose: (m, d) => emit(LogLevel.VERBOSE, m, d),
|
|
150
|
+
withContext(extra) {
|
|
151
|
+
return createLogger({
|
|
152
|
+
...params,
|
|
153
|
+
baseContext: { ...(baseContext || {}), ...extra },
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
profiled<TArgs extends any[], TRet>(
|
|
157
|
+
name: string,
|
|
158
|
+
func: (...args: TArgs) => TRet,
|
|
159
|
+
) {
|
|
160
|
+
return function profiledWrapper(this: any, ...args: TArgs): TRet {
|
|
161
|
+
const start = Date.now();
|
|
162
|
+
const result = func.apply(this, args) as TRet;
|
|
163
|
+
const finish = (ok: boolean) => {
|
|
164
|
+
const dur = Date.now() - start;
|
|
165
|
+
const msg = ok ? `profiled ${name}` : `profiled ${name} (failed)`;
|
|
166
|
+
// Use DEBUG for timing
|
|
167
|
+
emit(LogLevel.DEBUG, msg, {
|
|
168
|
+
executionTimeMs: dur,
|
|
169
|
+
functionName: name,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
const maybePromise = result as unknown as { then?: Function };
|
|
173
|
+
if (maybePromise && typeof maybePromise.then === "function") {
|
|
174
|
+
return (result as unknown as Promise<unknown>).then(
|
|
175
|
+
(r) => {
|
|
176
|
+
finish(true);
|
|
177
|
+
return r as TRet;
|
|
178
|
+
},
|
|
179
|
+
(e) => {
|
|
180
|
+
finish(false);
|
|
181
|
+
throw e;
|
|
182
|
+
},
|
|
183
|
+
) as unknown as TRet;
|
|
184
|
+
}
|
|
185
|
+
finish(true);
|
|
186
|
+
return result as TRet;
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return logger;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { LogLevel, consoleSink };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { InnerLogger } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* No-operation logger that discards all events.
|
|
5
|
+
* Used as a fallback when logger initialization fails or in disabled states.
|
|
6
|
+
*/
|
|
7
|
+
export const noopLogger: InnerLogger<any> = {
|
|
8
|
+
trackEvent: async () => {},
|
|
9
|
+
_internal: {
|
|
10
|
+
ready: Promise.resolve(),
|
|
11
|
+
anonId: "",
|
|
12
|
+
},
|
|
13
|
+
};
|