@lobu/core 3.0.5 → 3.0.7
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/package.json +3 -3
- package/src/__tests__/encryption.test.ts +103 -0
- package/src/__tests__/fixtures/factories.ts +76 -0
- package/src/__tests__/fixtures/index.ts +9 -0
- package/src/__tests__/fixtures/mock-fetch.ts +32 -0
- package/src/__tests__/fixtures/mock-queue.ts +50 -0
- package/src/__tests__/fixtures/mock-redis.ts +300 -0
- package/src/__tests__/retry.test.ts +134 -0
- package/src/__tests__/sanitize.test.ts +158 -0
- package/src/agent-policy.ts +207 -0
- package/src/agent-store.ts +220 -0
- package/src/api-types.ts +256 -0
- package/src/command-registry.ts +73 -0
- package/src/constants.ts +60 -0
- package/src/errors.ts +220 -0
- package/src/index.ts +131 -0
- package/src/integration-types.ts +26 -0
- package/src/logger.ts +248 -0
- package/src/modules.ts +184 -0
- package/src/otel.ts +306 -0
- package/src/plugin-types.ts +46 -0
- package/src/provider-config-types.ts +54 -0
- package/src/redis/base-store.ts +200 -0
- package/src/sentry.ts +54 -0
- package/src/trace.ts +32 -0
- package/src/types.ts +430 -0
- package/src/utils/encryption.ts +78 -0
- package/src/utils/env.ts +50 -0
- package/src/utils/json.ts +37 -0
- package/src/utils/lock.ts +75 -0
- package/src/utils/mcp-tool-instructions.ts +5 -0
- package/src/utils/retry.ts +91 -0
- package/src/utils/sanitize.ts +127 -0
- package/src/worker/auth.ts +100 -0
- package/src/worker/transport.ts +107 -0
- package/tsconfig.json +20 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all lobu errors
|
|
3
|
+
*/
|
|
4
|
+
export abstract class BaseError extends Error {
|
|
5
|
+
abstract readonly name: string;
|
|
6
|
+
public operation?: string;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
message: string,
|
|
10
|
+
public cause?: Error
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
|
|
14
|
+
// Maintain proper prototype chain for instanceof checks
|
|
15
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the full error chain as a string
|
|
20
|
+
*/
|
|
21
|
+
getFullMessage(): string {
|
|
22
|
+
let message = `${this.name}: ${this.message}`;
|
|
23
|
+
if (this.cause) {
|
|
24
|
+
if (this.cause instanceof BaseError) {
|
|
25
|
+
message += `\nCaused by: ${this.cause.getFullMessage()}`;
|
|
26
|
+
} else {
|
|
27
|
+
message += `\nCaused by: ${this.cause.message}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return message;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert error to JSON for logging/serialization
|
|
35
|
+
*/
|
|
36
|
+
toJSON(): Record<string, any> {
|
|
37
|
+
return {
|
|
38
|
+
name: this.name,
|
|
39
|
+
message: this.message,
|
|
40
|
+
...(this.operation && { operation: this.operation }),
|
|
41
|
+
cause:
|
|
42
|
+
this.cause instanceof BaseError
|
|
43
|
+
? this.cause.toJSON()
|
|
44
|
+
: this.cause?.message,
|
|
45
|
+
stack: this.stack,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Error class for worker-related operations
|
|
52
|
+
*/
|
|
53
|
+
export class WorkerError extends BaseError {
|
|
54
|
+
override readonly name = "WorkerError";
|
|
55
|
+
|
|
56
|
+
constructor(operation: string, message: string, cause?: Error) {
|
|
57
|
+
super(message, cause);
|
|
58
|
+
this.operation = operation;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Error class for workspace-related operations
|
|
64
|
+
*/
|
|
65
|
+
export class WorkspaceError extends BaseError {
|
|
66
|
+
override readonly name = "WorkspaceError";
|
|
67
|
+
|
|
68
|
+
constructor(operation: string, message: string, cause?: Error) {
|
|
69
|
+
super(message, cause);
|
|
70
|
+
this.operation = operation;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Error class for platform-related operations (Slack, WhatsApp, etc.)
|
|
76
|
+
*/
|
|
77
|
+
export class PlatformError extends BaseError {
|
|
78
|
+
override readonly name = "PlatformError";
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
public platform: string,
|
|
82
|
+
operation: string,
|
|
83
|
+
message: string,
|
|
84
|
+
cause?: Error
|
|
85
|
+
) {
|
|
86
|
+
super(message, cause);
|
|
87
|
+
this.operation = operation;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override toJSON(): Record<string, any> {
|
|
91
|
+
return {
|
|
92
|
+
...super.toJSON(),
|
|
93
|
+
platform: this.platform,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Error class for session-related operations
|
|
100
|
+
*/
|
|
101
|
+
export class SessionError extends BaseError {
|
|
102
|
+
readonly name = "SessionError";
|
|
103
|
+
|
|
104
|
+
constructor(
|
|
105
|
+
public sessionKey: string,
|
|
106
|
+
public code: string,
|
|
107
|
+
message: string,
|
|
108
|
+
cause?: Error
|
|
109
|
+
) {
|
|
110
|
+
super(message, cause);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
toJSON(): Record<string, any> {
|
|
114
|
+
return {
|
|
115
|
+
...super.toJSON(),
|
|
116
|
+
sessionKey: this.sessionKey,
|
|
117
|
+
code: this.code,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Worker error variant with workerId for core operations
|
|
124
|
+
*/
|
|
125
|
+
export class CoreWorkerError extends WorkerError {
|
|
126
|
+
constructor(
|
|
127
|
+
public workerId: string,
|
|
128
|
+
operation: string,
|
|
129
|
+
message: string,
|
|
130
|
+
cause?: Error
|
|
131
|
+
) {
|
|
132
|
+
super(operation, message, cause);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
override toJSON(): Record<string, any> {
|
|
136
|
+
return {
|
|
137
|
+
...super.toJSON(),
|
|
138
|
+
workerId: this.workerId,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Error class for dispatcher-related operations
|
|
145
|
+
*/
|
|
146
|
+
export class DispatcherError extends BaseError {
|
|
147
|
+
override readonly name = "DispatcherError";
|
|
148
|
+
|
|
149
|
+
constructor(operation: string, message: string, cause?: Error) {
|
|
150
|
+
super(message, cause);
|
|
151
|
+
this.operation = operation;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ErrorCode enum for orchestration operations
|
|
156
|
+
export enum ErrorCode {
|
|
157
|
+
DATABASE_CONNECTION_FAILED = "DATABASE_CONNECTION_FAILED",
|
|
158
|
+
KUBERNETES_API_ERROR = "KUBERNETES_API_ERROR",
|
|
159
|
+
DEPLOYMENT_SCALE_FAILED = "DEPLOYMENT_SCALE_FAILED",
|
|
160
|
+
DEPLOYMENT_CREATE_FAILED = "DEPLOYMENT_CREATE_FAILED",
|
|
161
|
+
DEPLOYMENT_DELETE_FAILED = "DEPLOYMENT_DELETE_FAILED",
|
|
162
|
+
QUEUE_JOB_PROCESSING_FAILED = "QUEUE_JOB_PROCESSING_FAILED",
|
|
163
|
+
USER_CREDENTIALS_CREATE_FAILED = "USER_CREDENTIALS_CREATE_FAILED",
|
|
164
|
+
INVALID_CONFIGURATION = "INVALID_CONFIGURATION",
|
|
165
|
+
THREAD_DEPLOYMENT_NOT_FOUND = "THREAD_DEPLOYMENT_NOT_FOUND",
|
|
166
|
+
USER_QUEUE_NOT_FOUND = "USER_QUEUE_NOT_FOUND",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Error class for orchestrator-related operations
|
|
171
|
+
*/
|
|
172
|
+
export class OrchestratorError extends BaseError {
|
|
173
|
+
readonly name = "OrchestratorError";
|
|
174
|
+
|
|
175
|
+
constructor(
|
|
176
|
+
public code: ErrorCode,
|
|
177
|
+
message: string,
|
|
178
|
+
public details?: any,
|
|
179
|
+
public shouldRetry: boolean = false,
|
|
180
|
+
cause?: Error
|
|
181
|
+
) {
|
|
182
|
+
super(message, cause);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static fromDatabaseError(error: any): OrchestratorError {
|
|
186
|
+
return new OrchestratorError(
|
|
187
|
+
ErrorCode.DATABASE_CONNECTION_FAILED,
|
|
188
|
+
`Database error: ${error instanceof Error ? error.message : String(error)}`,
|
|
189
|
+
{ code: error.code, detail: error.detail },
|
|
190
|
+
true,
|
|
191
|
+
error
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static fromKubernetesError(error: any): OrchestratorError {
|
|
196
|
+
return new OrchestratorError(
|
|
197
|
+
ErrorCode.KUBERNETES_API_ERROR,
|
|
198
|
+
`Kubernetes operation failed: ${error.message}`,
|
|
199
|
+
error,
|
|
200
|
+
true,
|
|
201
|
+
error
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
toJSON(): Record<string, any> {
|
|
206
|
+
return {
|
|
207
|
+
...super.toJSON(),
|
|
208
|
+
code: this.code,
|
|
209
|
+
details: this.details,
|
|
210
|
+
shouldRetry: this.shouldRetry,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Error class for configuration-related operations
|
|
217
|
+
*/
|
|
218
|
+
export class ConfigError extends BaseError {
|
|
219
|
+
readonly name = "ConfigError";
|
|
220
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// Shared exports for @lobu/core consumers (gateway, worker, external tools)
|
|
4
|
+
|
|
5
|
+
export * from "./agent-policy";
|
|
6
|
+
// Agent store interface (unified storage abstraction)
|
|
7
|
+
export type {
|
|
8
|
+
AgentAccessStore,
|
|
9
|
+
AgentConfigStore,
|
|
10
|
+
AgentConnectionStore,
|
|
11
|
+
AgentMetadata,
|
|
12
|
+
AgentSettings,
|
|
13
|
+
AgentStore,
|
|
14
|
+
ChannelBinding,
|
|
15
|
+
ConnectionSettings,
|
|
16
|
+
Grant,
|
|
17
|
+
StoredConnection,
|
|
18
|
+
} from "./agent-store";
|
|
19
|
+
export { findTemplateAgentId } from "./agent-store";
|
|
20
|
+
export type { CommandContext, CommandDefinition } from "./command-registry";
|
|
21
|
+
// Command registry
|
|
22
|
+
export { CommandRegistry } from "./command-registry";
|
|
23
|
+
export * from "./constants";
|
|
24
|
+
// Errors & logging
|
|
25
|
+
export * from "./errors";
|
|
26
|
+
// Integration types
|
|
27
|
+
export type {
|
|
28
|
+
SystemSkillEntry,
|
|
29
|
+
SystemSkillsConfigFile,
|
|
30
|
+
} from "./integration-types";
|
|
31
|
+
export * from "./logger";
|
|
32
|
+
// Module system
|
|
33
|
+
export type { ActionButton, ModuleSessionContext } from "./modules";
|
|
34
|
+
export * from "./modules";
|
|
35
|
+
export type { OtelConfig, Span, Tracer } from "./otel";
|
|
36
|
+
// OpenTelemetry tracing (Tempo integration)
|
|
37
|
+
export {
|
|
38
|
+
createChildSpan,
|
|
39
|
+
createRootSpan,
|
|
40
|
+
createSpan,
|
|
41
|
+
flushTracing,
|
|
42
|
+
getCurrentSpan,
|
|
43
|
+
getTraceparent,
|
|
44
|
+
getTracer,
|
|
45
|
+
initTracing,
|
|
46
|
+
runInSpanContext,
|
|
47
|
+
SpanKind,
|
|
48
|
+
SpanStatusCode,
|
|
49
|
+
shutdownTracing,
|
|
50
|
+
withChildSpan,
|
|
51
|
+
withSpan,
|
|
52
|
+
} from "./otel";
|
|
53
|
+
// Plugin types
|
|
54
|
+
export type {
|
|
55
|
+
PluginConfig,
|
|
56
|
+
PluginManifest,
|
|
57
|
+
PluginSlot,
|
|
58
|
+
PluginsConfig,
|
|
59
|
+
ProviderRegistration,
|
|
60
|
+
} from "./plugin-types";
|
|
61
|
+
// Config-driven provider types
|
|
62
|
+
export type {
|
|
63
|
+
ConfigProviderMeta,
|
|
64
|
+
ProviderConfigEntry,
|
|
65
|
+
} from "./provider-config-types";
|
|
66
|
+
// Redis & worker helpers
|
|
67
|
+
export * from "./redis/base-store";
|
|
68
|
+
// Observability
|
|
69
|
+
export { getSentry, initSentry } from "./sentry";
|
|
70
|
+
export { extractTraceId, generateTraceId } from "./trace";
|
|
71
|
+
// Core types
|
|
72
|
+
export type {
|
|
73
|
+
AgentMcpConfig,
|
|
74
|
+
AgentOptions,
|
|
75
|
+
AuthProfile,
|
|
76
|
+
CliBackendConfig,
|
|
77
|
+
ConversationMessage,
|
|
78
|
+
HistoryMessage,
|
|
79
|
+
InstalledProvider,
|
|
80
|
+
InstructionContext,
|
|
81
|
+
InstructionProvider,
|
|
82
|
+
LogLevel,
|
|
83
|
+
McpServerConfig,
|
|
84
|
+
NetworkConfig,
|
|
85
|
+
NixConfig,
|
|
86
|
+
RegistryEntry,
|
|
87
|
+
SessionContext,
|
|
88
|
+
SkillConfig,
|
|
89
|
+
SkillMcpServer,
|
|
90
|
+
SkillsConfig,
|
|
91
|
+
SuggestedPrompt,
|
|
92
|
+
ThinkingLevel,
|
|
93
|
+
ThreadResponsePayload,
|
|
94
|
+
ToolsConfig,
|
|
95
|
+
UserSuggestion,
|
|
96
|
+
} from "./types";
|
|
97
|
+
|
|
98
|
+
// Agent Settings API response types (for UI consumers)
|
|
99
|
+
export type {
|
|
100
|
+
AgentConfigResponse,
|
|
101
|
+
AgentInfo,
|
|
102
|
+
CatalogProvider,
|
|
103
|
+
Connection,
|
|
104
|
+
McpConfig,
|
|
105
|
+
ModelOption,
|
|
106
|
+
ModelSelectionState,
|
|
107
|
+
PermissionGrant,
|
|
108
|
+
PrefillMcp,
|
|
109
|
+
PrefillSkill,
|
|
110
|
+
ProviderInfo,
|
|
111
|
+
ProviderState,
|
|
112
|
+
ProviderStatus,
|
|
113
|
+
Schedule,
|
|
114
|
+
SettingsSnapshot,
|
|
115
|
+
Skill,
|
|
116
|
+
SkillMcpServerInfo,
|
|
117
|
+
} from "./api-types";
|
|
118
|
+
|
|
119
|
+
// Utilities
|
|
120
|
+
export * from "./utils/encryption";
|
|
121
|
+
export * from "./utils/env";
|
|
122
|
+
export * from "./utils/json";
|
|
123
|
+
export * from "./utils/lock";
|
|
124
|
+
export type { McpToolDef } from "./utils/mcp-tool-instructions";
|
|
125
|
+
export * from "./utils/retry";
|
|
126
|
+
export * from "./utils/sanitize";
|
|
127
|
+
export * from "./worker/auth";
|
|
128
|
+
export type {
|
|
129
|
+
WorkerTransport,
|
|
130
|
+
WorkerTransportConfig,
|
|
131
|
+
} from "./worker/transport";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the integration system.
|
|
3
|
+
*
|
|
4
|
+
* OAuth credential management for third-party APIs (GitHub, Google, etc.)
|
|
5
|
+
* is handled by Owletto.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ProviderConfigEntry } from "./provider-config-types";
|
|
9
|
+
|
|
10
|
+
// System Skills Config (config/system-skills.json)
|
|
11
|
+
|
|
12
|
+
export interface SystemSkillEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
instructions?: string;
|
|
17
|
+
hidden?: boolean;
|
|
18
|
+
mcpServers?: import("./types").SkillMcpServer[];
|
|
19
|
+
providers?: ProviderConfigEntry[];
|
|
20
|
+
nixPackages?: string[];
|
|
21
|
+
permissions?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SystemSkillsConfigFile {
|
|
25
|
+
skills: SystemSkillEntry[];
|
|
26
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Use simple console.log-based logger by default (unbuffered, 12-factor compliant)
|
|
2
|
+
// Set USE_WINSTON_LOGGER=true only if you need Winston features (file rotation, multiple transports)
|
|
3
|
+
const USE_WINSTON_LOGGER = process.env.USE_WINSTON_LOGGER === "true";
|
|
4
|
+
// Use JSON format for structured logging (better for Loki parsing in production)
|
|
5
|
+
const USE_JSON_FORMAT = process.env.LOG_FORMAT === "json";
|
|
6
|
+
|
|
7
|
+
import winston from "winston";
|
|
8
|
+
import { getSentry } from "./sentry";
|
|
9
|
+
|
|
10
|
+
export interface Logger {
|
|
11
|
+
error: (message: any, ...args: any[]) => void;
|
|
12
|
+
warn: (message: any, ...args: any[]) => void;
|
|
13
|
+
info: (message: any, ...args: any[]) => void;
|
|
14
|
+
debug: (message: any, ...args: any[]) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Simple console logger fallback for environments where Winston doesn't work (Bun + Alpine)
|
|
18
|
+
// Supports both formats: logger.info("message", data) AND pino-style logger.info({ data }, "message")
|
|
19
|
+
function createConsoleLogger(serviceName: string): Logger {
|
|
20
|
+
const level = process.env.LOG_LEVEL || "info";
|
|
21
|
+
const levels: Record<string, number> = {
|
|
22
|
+
error: 0,
|
|
23
|
+
warn: 1,
|
|
24
|
+
info: 2,
|
|
25
|
+
debug: 3,
|
|
26
|
+
};
|
|
27
|
+
const currentLevel = levels[level] ?? 2;
|
|
28
|
+
|
|
29
|
+
const formatMessage = (lvl: string, message: any, ...args: any[]): string => {
|
|
30
|
+
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
31
|
+
let msgStr: string;
|
|
32
|
+
let meta: any = null;
|
|
33
|
+
|
|
34
|
+
// Handle pino-style format: logger.info({ key: value }, "message")
|
|
35
|
+
if (
|
|
36
|
+
typeof message === "object" &&
|
|
37
|
+
message !== null &&
|
|
38
|
+
!Array.isArray(message) &&
|
|
39
|
+
!(message instanceof Error)
|
|
40
|
+
) {
|
|
41
|
+
if (args.length > 0 && typeof args[0] === "string") {
|
|
42
|
+
// First arg is metadata object, second arg is the actual message
|
|
43
|
+
msgStr = args[0];
|
|
44
|
+
meta = message;
|
|
45
|
+
args = args.slice(1);
|
|
46
|
+
} else {
|
|
47
|
+
// Just an object, stringify it
|
|
48
|
+
try {
|
|
49
|
+
msgStr = JSON.stringify(message);
|
|
50
|
+
} catch {
|
|
51
|
+
msgStr = "[object]";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
msgStr = String(message);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Append remaining args
|
|
59
|
+
if (args.length > 0) {
|
|
60
|
+
try {
|
|
61
|
+
msgStr += ` ${JSON.stringify(args.length === 1 ? args[0] : args)}`;
|
|
62
|
+
} catch {
|
|
63
|
+
msgStr += " [unserializable]";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Append metadata object
|
|
68
|
+
if (meta) {
|
|
69
|
+
try {
|
|
70
|
+
msgStr += ` ${JSON.stringify(meta)}`;
|
|
71
|
+
} catch {
|
|
72
|
+
msgStr += " [meta unserializable]";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `[${timestamp}] [${lvl}] [${serviceName}] ${msgStr}`;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
error: (message: any, ...args: any[]) => {
|
|
81
|
+
if (currentLevel >= 0)
|
|
82
|
+
console.error(formatMessage("error", message, ...args));
|
|
83
|
+
},
|
|
84
|
+
warn: (message: any, ...args: any[]) => {
|
|
85
|
+
if (currentLevel >= 1)
|
|
86
|
+
console.warn(formatMessage("warn", message, ...args));
|
|
87
|
+
},
|
|
88
|
+
info: (message: any, ...args: any[]) => {
|
|
89
|
+
if (currentLevel >= 2)
|
|
90
|
+
console.log(formatMessage("info", message, ...args));
|
|
91
|
+
},
|
|
92
|
+
debug: (message: any, ...args: any[]) => {
|
|
93
|
+
if (currentLevel >= 3)
|
|
94
|
+
console.log(formatMessage("debug", message, ...args));
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Custom Winston transport that sends errors to Sentry
|
|
101
|
+
*/
|
|
102
|
+
class SentryTransport extends winston.transports.Stream {
|
|
103
|
+
constructor() {
|
|
104
|
+
super({ stream: process.stdout });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
log(info: any, callback: () => void) {
|
|
108
|
+
setImmediate(() => {
|
|
109
|
+
this.emit("logged", info);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Only send errors and warnings to Sentry
|
|
113
|
+
if (info.level === "error" || info.level === "warn") {
|
|
114
|
+
const Sentry = getSentry();
|
|
115
|
+
if (Sentry) {
|
|
116
|
+
try {
|
|
117
|
+
// Extract error object if present
|
|
118
|
+
const errorObj =
|
|
119
|
+
info.error || (info.message instanceof Error ? info.message : null);
|
|
120
|
+
|
|
121
|
+
if (errorObj instanceof Error) {
|
|
122
|
+
Sentry.captureException(errorObj, {
|
|
123
|
+
level: info.level === "error" ? "error" : "warning",
|
|
124
|
+
tags: {
|
|
125
|
+
service: info.service,
|
|
126
|
+
},
|
|
127
|
+
extra: {
|
|
128
|
+
...info,
|
|
129
|
+
message: info.message,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
// Send as message if no Error object
|
|
134
|
+
Sentry.captureMessage(String(info.message), {
|
|
135
|
+
level: info.level === "error" ? "error" : "warning",
|
|
136
|
+
tags: {
|
|
137
|
+
service: info.service,
|
|
138
|
+
},
|
|
139
|
+
extra: info,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} catch (_err) {
|
|
143
|
+
// Ignore Sentry errors to avoid breaking logging
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
callback();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Creates a logger instance for a specific service
|
|
154
|
+
* Provides consistent logging format across all packages with level and timestamp
|
|
155
|
+
* @param serviceName The name of the service using the logger
|
|
156
|
+
* @returns A console logger by default, or Winston logger if USE_WINSTON_LOGGER=true
|
|
157
|
+
*/
|
|
158
|
+
export function createLogger(serviceName: string): Logger {
|
|
159
|
+
// Use simple console.log logger by default (unbuffered, 12-factor compliant)
|
|
160
|
+
// Set USE_WINSTON_LOGGER=true for Winston features (file rotation, multiple transports)
|
|
161
|
+
if (!USE_WINSTON_LOGGER) {
|
|
162
|
+
return createConsoleLogger(serviceName);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
166
|
+
const level = process.env.LOG_LEVEL || "info";
|
|
167
|
+
|
|
168
|
+
// JSON format for structured logging (better for Loki/Grafana parsing)
|
|
169
|
+
const jsonFormat = winston.format.combine(
|
|
170
|
+
winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }),
|
|
171
|
+
winston.format.json()
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Human-readable format for development
|
|
175
|
+
const humanFormat = winston.format.combine(
|
|
176
|
+
...(isProduction ? [] : [winston.format.colorize()]),
|
|
177
|
+
winston.format.printf(({ timestamp, level, message, service, ...meta }) => {
|
|
178
|
+
let metaStr = "";
|
|
179
|
+
if (Object.keys(meta).length) {
|
|
180
|
+
try {
|
|
181
|
+
metaStr = ` ${JSON.stringify(meta, null, 0)}`;
|
|
182
|
+
} catch (_err) {
|
|
183
|
+
// Handle circular structures with a safer approach
|
|
184
|
+
try {
|
|
185
|
+
const seen = new WeakSet();
|
|
186
|
+
metaStr = ` ${JSON.stringify(meta, (_key, value) => {
|
|
187
|
+
if (typeof value === "object" && value !== null) {
|
|
188
|
+
if (seen.has(value)) {
|
|
189
|
+
return "[Circular Reference]";
|
|
190
|
+
}
|
|
191
|
+
seen.add(value);
|
|
192
|
+
|
|
193
|
+
if (value instanceof Error) {
|
|
194
|
+
return {
|
|
195
|
+
name: value.name,
|
|
196
|
+
message: value.message,
|
|
197
|
+
stack: value.stack?.split("\n")[0], // Only first line of stack
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
})}`;
|
|
203
|
+
} catch (_err2) {
|
|
204
|
+
// Final fallback if even the circular handler fails
|
|
205
|
+
metaStr = " [Object too complex to serialize]";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return `[${timestamp}] [${level}] [${service}] ${message}${metaStr}`;
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const transports: winston.transport[] = [
|
|
214
|
+
new winston.transports.Console({
|
|
215
|
+
format: USE_JSON_FORMAT ? jsonFormat : humanFormat,
|
|
216
|
+
}),
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const logger = winston.createLogger({
|
|
220
|
+
level,
|
|
221
|
+
format: winston.format.combine(
|
|
222
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
223
|
+
winston.format.errors({ stack: true }),
|
|
224
|
+
winston.format.splat()
|
|
225
|
+
),
|
|
226
|
+
defaultMeta: { service: serviceName },
|
|
227
|
+
transports,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Add Sentry transport in production or if SENTRY_DSN is set
|
|
231
|
+
// Deferred to avoid circular dependency with sentry.ts
|
|
232
|
+
// The check is inside setImmediate to ensure SentryTransport class is fully initialized
|
|
233
|
+
setImmediate(() => {
|
|
234
|
+
if (isProduction || process.env.SENTRY_DSN) {
|
|
235
|
+
try {
|
|
236
|
+
const transport = new SentryTransport();
|
|
237
|
+
logger.add(transport);
|
|
238
|
+
} catch {
|
|
239
|
+
// Ignore errors during Sentry transport setup
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return logger;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Default logger instance for backward compatibility
|
|
248
|
+
export const logger = createLogger("shared");
|