@open-mercato/shared 0.6.7-develop.6828.1.ab1620a63e → 0.6.7-develop.6842.1.8cd8a8883c
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -1
- package/dist/lib/bootstrap/dynamicLoader.js +54 -14
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/di/container.js +12 -1
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/redis/connection.js +7 -2
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +192 -0
- package/src/lib/bootstrap/dynamicLoader.ts +111 -32
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +17 -1
- package/src/lib/di/container.ts +19 -1
- package/src/lib/i18n/__tests__/config.test.ts +8 -0
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/redis/__tests__/connection.test.ts +16 -0
- package/src/lib/redis/connection.ts +10 -7
- package/src/lib/telemetry/runtime.ts +72 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { LogLevel } from './level'
|
|
2
|
+
|
|
3
|
+
export type LoggerExtensionRecord = {
|
|
4
|
+
level: LogLevel
|
|
5
|
+
namespace: string
|
|
6
|
+
message: string
|
|
7
|
+
fields: Record<string, unknown>
|
|
8
|
+
time: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type LoggerExtension = {
|
|
12
|
+
/**
|
|
13
|
+
* Add process-local context (for example trace/span ids) to both the local
|
|
14
|
+
* line and the optional remote sink.
|
|
15
|
+
*/
|
|
16
|
+
enrich?(): Record<string, unknown> | undefined
|
|
17
|
+
/**
|
|
18
|
+
* Observe a record after the local transport writes it. Implementations must
|
|
19
|
+
* never throw into application code.
|
|
20
|
+
*/
|
|
21
|
+
emit?(record: LoggerExtensionRecord): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const GLOBAL_KEY = Symbol.for('@open-mercato/shared.loggerExtension')
|
|
25
|
+
|
|
26
|
+
type LoggerExtensionStore = {
|
|
27
|
+
active?: LoggerExtension
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function store(): LoggerExtensionStore {
|
|
31
|
+
const globalStore = globalThis as unknown as Record<symbol, LoggerExtensionStore | undefined>
|
|
32
|
+
let current = globalStore[GLOBAL_KEY]
|
|
33
|
+
if (!current) {
|
|
34
|
+
current = {}
|
|
35
|
+
globalStore[GLOBAL_KEY] = current
|
|
36
|
+
}
|
|
37
|
+
return current
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Register the single process-wide logger extension. The returned disposer only
|
|
42
|
+
* clears the extension when it is still the active registration.
|
|
43
|
+
*/
|
|
44
|
+
export function registerLoggerExtension(extension: LoggerExtension): () => void {
|
|
45
|
+
store().active = extension
|
|
46
|
+
return () => {
|
|
47
|
+
const current = store()
|
|
48
|
+
if (current.active === extension) current.active = undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getLoggerExtension(): LoggerExtension | undefined {
|
|
53
|
+
return store().active
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Test-only: clear the process-wide extension. */
|
|
57
|
+
export function resetLoggerExtension(): void {
|
|
58
|
+
store().active = undefined
|
|
59
|
+
}
|
package/src/lib/logger/index.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { selectTransport } from './transport'
|
|
2
|
+
import { getLoggerExtension } from './extension'
|
|
3
|
+
import { isLevelEnabled, type LogLevel } from './level'
|
|
2
4
|
|
|
3
5
|
export type { LogLevel } from './level'
|
|
4
6
|
export { getLogLevel, isLevelEnabled, resetLogLevelCache, OM_LOG_LEVEL_ENV } from './level'
|
|
5
7
|
export { resetServerLoggerCache, OM_LOG_DESTINATION_ENV } from './transport.server'
|
|
6
8
|
export { resetLogPrettyCache, OM_LOG_PRETTY_ENV } from './transport.pretty'
|
|
9
|
+
export {
|
|
10
|
+
getLoggerExtension,
|
|
11
|
+
registerLoggerExtension,
|
|
12
|
+
resetLoggerExtension,
|
|
13
|
+
} from './extension'
|
|
14
|
+
export type { LoggerExtension, LoggerExtensionRecord } from './extension'
|
|
7
15
|
|
|
8
16
|
export type LogBindings = Record<string, unknown>
|
|
9
17
|
|
|
@@ -18,11 +26,61 @@ export interface Logger {
|
|
|
18
26
|
|
|
19
27
|
const loggerRegistry = new Map<string, Logger>()
|
|
20
28
|
|
|
29
|
+
function createExtendedLogger(
|
|
30
|
+
namespace: string,
|
|
31
|
+
transport: Logger,
|
|
32
|
+
bindings: LogBindings = {},
|
|
33
|
+
): Logger {
|
|
34
|
+
const emit = (
|
|
35
|
+
level: LogLevel,
|
|
36
|
+
msg: string,
|
|
37
|
+
fields?: LogBindings,
|
|
38
|
+
): void => {
|
|
39
|
+
if (!isLevelEnabled(level)) return
|
|
40
|
+
|
|
41
|
+
const extension = getLoggerExtension()
|
|
42
|
+
let context: LogBindings = {}
|
|
43
|
+
try {
|
|
44
|
+
context = extension?.enrich?.() ?? {}
|
|
45
|
+
} catch {
|
|
46
|
+
// Observability must never alter application behavior.
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mergedFields = { ...fields, ...context }
|
|
50
|
+
transport[level](msg, mergedFields)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
extension?.emit?.({
|
|
54
|
+
level,
|
|
55
|
+
namespace,
|
|
56
|
+
message: msg,
|
|
57
|
+
fields: { ...bindings, ...mergedFields },
|
|
58
|
+
time: Date.now(),
|
|
59
|
+
})
|
|
60
|
+
} catch {
|
|
61
|
+
// Remote logging is best-effort; the local line has already been written.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
debug: (msg, fields) => emit('debug', msg, fields),
|
|
67
|
+
info: (msg, fields) => emit('info', msg, fields),
|
|
68
|
+
warn: (msg, fields) => emit('warn', msg, fields),
|
|
69
|
+
error: (msg, fields) => emit('error', msg, fields),
|
|
70
|
+
child: (childBindings) =>
|
|
71
|
+
createExtendedLogger(
|
|
72
|
+
namespace,
|
|
73
|
+
transport.child(childBindings),
|
|
74
|
+
{ ...bindings, ...childBindings },
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
21
79
|
/** Create (or reuse) a namespaced logger. `namespace` is attached as `name`. */
|
|
22
80
|
export function createLogger(namespace: string): Logger {
|
|
23
81
|
const existing = loggerRegistry.get(namespace)
|
|
24
82
|
if (existing) return existing
|
|
25
|
-
const created = selectTransport(namespace)
|
|
83
|
+
const created = createExtendedLogger(namespace, selectTransport(namespace))
|
|
26
84
|
loggerRegistry.set(namespace, created)
|
|
27
85
|
return created
|
|
28
86
|
}
|
|
@@ -69,3 +69,13 @@ export function getModules(): Module[] {
|
|
|
69
69
|
}
|
|
70
70
|
return modules
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Non-throwing counterpart of `getModules()` for call sites that have a
|
|
75
|
+
* meaningful degraded behavior when bootstrap has not run — route unit tests
|
|
76
|
+
* exercise a single handler without `registerModules()`, and a hard throw there
|
|
77
|
+
* turns an unrelated assertion into a bootstrap error.
|
|
78
|
+
*/
|
|
79
|
+
export function tryGetModules(): Module[] | null {
|
|
80
|
+
return getGlobalModules()
|
|
81
|
+
}
|
|
@@ -5,9 +5,11 @@ describe('parseRedisUrl', () => {
|
|
|
5
5
|
expect(parseRedisUrl('rediss://:secret@example.cache.amazonaws.com:6379/2')).toEqual({
|
|
6
6
|
host: 'example.cache.amazonaws.com',
|
|
7
7
|
port: 6379,
|
|
8
|
+
username: undefined,
|
|
8
9
|
password: 'secret',
|
|
9
10
|
db: 2,
|
|
10
11
|
tls: {},
|
|
12
|
+
family: undefined,
|
|
11
13
|
})
|
|
12
14
|
})
|
|
13
15
|
|
|
@@ -15,9 +17,23 @@ describe('parseRedisUrl', () => {
|
|
|
15
17
|
expect(parseRedisUrl('redis://:secret@localhost:6379/0')).toEqual({
|
|
16
18
|
host: 'localhost',
|
|
17
19
|
port: 6379,
|
|
20
|
+
username: undefined,
|
|
18
21
|
password: 'secret',
|
|
19
22
|
db: 0,
|
|
20
23
|
tls: undefined,
|
|
24
|
+
family: undefined,
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('preserves ACL credentials and supported address-family options', () => {
|
|
29
|
+
expect(parseRedisUrl('rediss://app%2Duser:p%40ss@example.com:6380/4?family=6')).toEqual({
|
|
30
|
+
host: 'example.com',
|
|
31
|
+
port: 6380,
|
|
32
|
+
username: 'app-user',
|
|
33
|
+
password: 'p@ss',
|
|
34
|
+
db: 4,
|
|
35
|
+
tls: {},
|
|
36
|
+
family: 6,
|
|
21
37
|
})
|
|
22
38
|
})
|
|
23
39
|
})
|
|
@@ -21,9 +21,11 @@ const logger = createLogger('shared').child({ component: 'redis' })
|
|
|
21
21
|
export type ParsedRedisConnection = {
|
|
22
22
|
host: string
|
|
23
23
|
port: number
|
|
24
|
+
username?: string
|
|
24
25
|
password?: string
|
|
25
26
|
db?: number
|
|
26
27
|
tls?: Record<string, unknown>
|
|
28
|
+
family?: number
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -59,12 +61,8 @@ export function getRedisUrlOrThrow(prefix?: string): string {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
|
-
* Parse a redis:// URL into
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @deprecated Prefer passing the full URL via `{ url: getRedisUrl(...) }` to
|
|
66
|
-
* BullMQ/ioredis — this preserves rediss://, username, database, and query
|
|
67
|
-
* params that structured parsing may lose. Kept for backward compatibility.
|
|
64
|
+
* Parse a redis:// URL into structured connection options suitable for
|
|
65
|
+
* BullMQ and ioredis.
|
|
68
66
|
*/
|
|
69
67
|
export function parseRedisUrl(url: string): ParsedRedisConnection {
|
|
70
68
|
try {
|
|
@@ -72,12 +70,17 @@ export function parseRedisUrl(url: string): ParsedRedisConnection {
|
|
|
72
70
|
const dbStr = parsed.pathname ? parsed.pathname.slice(1) : ''
|
|
73
71
|
const dbParsed = dbStr !== '' ? parseInt(dbStr, 10) : NaN
|
|
74
72
|
const db = Number.isNaN(dbParsed) ? undefined : dbParsed
|
|
73
|
+
const familyValue = parsed.searchParams.get('family')
|
|
74
|
+
const familyParsed = familyValue !== null ? parseInt(familyValue, 10) : NaN
|
|
75
|
+
const family = [0, 4, 6].includes(familyParsed) ? familyParsed : undefined
|
|
75
76
|
return {
|
|
76
77
|
host: parsed.hostname || 'localhost',
|
|
77
78
|
port: parseInt(parsed.port, 10) || 6379,
|
|
78
|
-
|
|
79
|
+
username: parsed.username ? decodeURIComponent(parsed.username) : undefined,
|
|
80
|
+
password: parsed.password ? decodeURIComponent(parsed.password) : undefined,
|
|
79
81
|
db,
|
|
80
82
|
tls: parsed.protocol === 'rediss:' ? {} : undefined,
|
|
83
|
+
family,
|
|
81
84
|
}
|
|
82
85
|
} catch {
|
|
83
86
|
const safeUrl = url.replace(/\/\/[^:]*:[^@]*@/, '//<redacted>@')
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type TelemetryTraceCarrier = Record<string, string>
|
|
2
|
+
|
|
3
|
+
export type TelemetryRuntime = {
|
|
4
|
+
/**
|
|
5
|
+
* True only when the active SDK may safely use the process-global W3C
|
|
6
|
+
* propagator for cross-boundary extraction.
|
|
7
|
+
*/
|
|
8
|
+
canUseGlobalTracePropagation(): boolean
|
|
9
|
+
captureTraceContext(): TelemetryTraceCarrier
|
|
10
|
+
continueTrace<T>(
|
|
11
|
+
carrier: TelemetryTraceCarrier | undefined,
|
|
12
|
+
name: string,
|
|
13
|
+
fn: () => T,
|
|
14
|
+
options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },
|
|
15
|
+
): T
|
|
16
|
+
recordHttpDuration(method: string, route: string, status: number, startedAt: number): void
|
|
17
|
+
reportError(
|
|
18
|
+
error: unknown,
|
|
19
|
+
context?: {
|
|
20
|
+
module?: string
|
|
21
|
+
attributes?: Record<string, string | number | boolean | undefined>
|
|
22
|
+
},
|
|
23
|
+
): void
|
|
24
|
+
shutdown(): Promise<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')
|
|
28
|
+
const ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])
|
|
29
|
+
|
|
30
|
+
type TelemetryRuntimeStore = {
|
|
31
|
+
active?: TelemetryRuntime
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function store(): TelemetryRuntimeStore {
|
|
35
|
+
const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>
|
|
36
|
+
let current = globalStore[GLOBAL_KEY]
|
|
37
|
+
if (!current) {
|
|
38
|
+
current = {}
|
|
39
|
+
globalStore[GLOBAL_KEY] = current
|
|
40
|
+
}
|
|
41
|
+
return current
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* This check is intentionally owned by shared code so hosts can decide whether
|
|
46
|
+
* to dynamically import the telemetry package without evaluating that package.
|
|
47
|
+
*/
|
|
48
|
+
export function isTelemetryBackendEnabled(raw?: string): boolean {
|
|
49
|
+
const value = raw ?? (
|
|
50
|
+
typeof process === 'undefined'
|
|
51
|
+
? undefined
|
|
52
|
+
: process.env.TELEMETRY_BACKEND
|
|
53
|
+
)
|
|
54
|
+
return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {
|
|
58
|
+
store().active = runtime
|
|
59
|
+
return () => {
|
|
60
|
+
const current = store()
|
|
61
|
+
if (current.active === runtime) current.active = undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getTelemetryRuntime(): TelemetryRuntime | undefined {
|
|
66
|
+
return store().active
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Test-only: clear the process-wide telemetry bridge. */
|
|
70
|
+
export function resetTelemetryRuntime(): void {
|
|
71
|
+
store().active = undefined
|
|
72
|
+
}
|