@artemiskit/core 0.1.5 → 0.2.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +1 -0
  3. package/dist/adapters/types.d.ts +3 -1
  4. package/dist/adapters/types.d.ts.map +1 -1
  5. package/dist/artifacts/types.d.ts +39 -0
  6. package/dist/artifacts/types.d.ts.map +1 -1
  7. package/dist/cost/index.d.ts +5 -0
  8. package/dist/cost/index.d.ts.map +1 -0
  9. package/dist/cost/pricing.d.ts +66 -0
  10. package/dist/cost/pricing.d.ts.map +1 -0
  11. package/dist/evaluators/combined.d.ts +10 -0
  12. package/dist/evaluators/combined.d.ts.map +1 -0
  13. package/dist/evaluators/index.d.ts +4 -0
  14. package/dist/evaluators/index.d.ts.map +1 -1
  15. package/dist/evaluators/inline.d.ts +22 -0
  16. package/dist/evaluators/inline.d.ts.map +1 -0
  17. package/dist/evaluators/not-contains.d.ts +10 -0
  18. package/dist/evaluators/not-contains.d.ts.map +1 -0
  19. package/dist/evaluators/similarity.d.ts +16 -0
  20. package/dist/evaluators/similarity.d.ts.map +1 -0
  21. package/dist/events/emitter.d.ts +111 -0
  22. package/dist/events/emitter.d.ts.map +1 -0
  23. package/dist/events/index.d.ts +6 -0
  24. package/dist/events/index.d.ts.map +1 -0
  25. package/dist/events/types.d.ts +177 -0
  26. package/dist/events/types.d.ts.map +1 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +16904 -18362
  30. package/dist/scenario/discovery.d.ts +72 -0
  31. package/dist/scenario/discovery.d.ts.map +1 -0
  32. package/dist/scenario/index.d.ts +1 -0
  33. package/dist/scenario/index.d.ts.map +1 -1
  34. package/dist/scenario/schema.d.ts +1245 -9
  35. package/dist/scenario/schema.d.ts.map +1 -1
  36. package/dist/utils/logger.d.ts.map +1 -1
  37. package/package.json +5 -6
  38. package/src/adapters/types.ts +3 -1
  39. package/src/artifacts/types.ts +39 -0
  40. package/src/cost/index.ts +14 -0
  41. package/src/cost/pricing.ts +273 -0
  42. package/src/evaluators/combined.test.ts +172 -0
  43. package/src/evaluators/combined.ts +95 -0
  44. package/src/evaluators/index.ts +12 -0
  45. package/src/evaluators/inline.test.ts +409 -0
  46. package/src/evaluators/inline.ts +393 -0
  47. package/src/evaluators/not-contains.test.ts +105 -0
  48. package/src/evaluators/not-contains.ts +45 -0
  49. package/src/evaluators/similarity.test.ts +333 -0
  50. package/src/evaluators/similarity.ts +258 -0
  51. package/src/index.ts +3 -0
  52. package/src/scenario/discovery.test.ts +153 -0
  53. package/src/scenario/discovery.ts +277 -0
  54. package/src/scenario/index.ts +1 -0
  55. package/src/scenario/schema.ts +43 -2
  56. package/src/utils/logger.ts +45 -16
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Tests for scenario discovery
3
+ */
4
+
5
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
6
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { discoverScenarios, matchScenarioGlob, resolveScenarioPaths } from './discovery';
10
+
11
+ describe('Scenario Discovery', () => {
12
+ const testDir = join(tmpdir(), `artemis-discovery-test-${Date.now()}`);
13
+
14
+ beforeAll(async () => {
15
+ // Create test directory structure
16
+ await mkdir(testDir, { recursive: true });
17
+ await mkdir(join(testDir, 'scenarios'), { recursive: true });
18
+ await mkdir(join(testDir, 'scenarios', 'auth'), { recursive: true });
19
+ await mkdir(join(testDir, 'scenarios', 'api'), { recursive: true });
20
+ await mkdir(join(testDir, 'node_modules'), { recursive: true });
21
+ await mkdir(join(testDir, 'drafts'), { recursive: true });
22
+
23
+ // Create test scenario files
24
+ const scenarioContent =
25
+ 'name: test\ncases:\n - id: t1\n prompt: test\n expected:\n type: exact\n value: test';
26
+
27
+ await writeFile(join(testDir, 'scenarios', 'basic.yaml'), scenarioContent);
28
+ await writeFile(join(testDir, 'scenarios', 'advanced.yml'), scenarioContent);
29
+ await writeFile(join(testDir, 'scenarios', 'auth', 'login.yaml'), scenarioContent);
30
+ await writeFile(join(testDir, 'scenarios', 'auth', 'logout.yaml'), scenarioContent);
31
+ await writeFile(join(testDir, 'scenarios', 'api', 'users.yaml'), scenarioContent);
32
+ await writeFile(join(testDir, 'scenarios', 'api', 'posts.yml'), scenarioContent);
33
+ await writeFile(join(testDir, 'node_modules', 'ignored.yaml'), scenarioContent);
34
+ await writeFile(join(testDir, 'drafts', 'draft.yaml'), scenarioContent);
35
+ await writeFile(join(testDir, 'scenarios', 'skip.draft.yaml'), scenarioContent);
36
+ await writeFile(join(testDir, 'scenarios', 'readme.md'), '# Not a scenario');
37
+ });
38
+
39
+ afterAll(async () => {
40
+ // Cleanup test directory
41
+ await rm(testDir, { recursive: true, force: true });
42
+ });
43
+
44
+ describe('discoverScenarios', () => {
45
+ test('finds all yaml and yml files in directory', async () => {
46
+ const files = await discoverScenarios(join(testDir, 'scenarios'));
47
+ expect(files.length).toBe(7); // 2 root + 2 auth + 2 api + 1 draft
48
+ });
49
+
50
+ test('excludes node_modules by default', async () => {
51
+ const files = await discoverScenarios(testDir);
52
+ const nodeModulesFiles = files.filter((f) => f.includes('node_modules'));
53
+ expect(nodeModulesFiles.length).toBe(0);
54
+ });
55
+
56
+ test('respects custom exclude patterns', async () => {
57
+ const files = await discoverScenarios(join(testDir, 'scenarios'), {
58
+ exclude: ['*.draft.yaml'],
59
+ });
60
+ const draftFiles = files.filter((f) => f.includes('.draft.yaml'));
61
+ expect(draftFiles.length).toBe(0);
62
+ });
63
+
64
+ test('respects custom extensions', async () => {
65
+ const files = await discoverScenarios(join(testDir, 'scenarios'), {
66
+ extensions: ['.yaml'],
67
+ });
68
+ const ymlFiles = files.filter((f) => f.endsWith('.yml'));
69
+ expect(ymlFiles.length).toBe(0);
70
+ });
71
+
72
+ test('respects maxDepth option', async () => {
73
+ const files = await discoverScenarios(join(testDir, 'scenarios'), {
74
+ maxDepth: 0,
75
+ });
76
+ // Should only find files in the root scenarios directory
77
+ expect(files.length).toBe(3); // basic.yaml, advanced.yml, skip.draft.yaml
78
+ });
79
+
80
+ test('throws error for non-directory path', async () => {
81
+ await expect(discoverScenarios(join(testDir, 'scenarios', 'basic.yaml'))).rejects.toThrow(
82
+ 'not a directory'
83
+ );
84
+ });
85
+
86
+ test('returns sorted results', async () => {
87
+ const files = await discoverScenarios(join(testDir, 'scenarios'));
88
+ const sorted = [...files].sort();
89
+ expect(files).toEqual(sorted);
90
+ });
91
+ });
92
+
93
+ describe('matchScenarioGlob', () => {
94
+ test('matches simple wildcard pattern', async () => {
95
+ const files = await matchScenarioGlob('scenarios/*.yaml', testDir);
96
+ expect(files.length).toBe(2); // basic.yaml, skip.draft.yaml
97
+ });
98
+
99
+ test('matches recursive pattern with **', async () => {
100
+ const files = await matchScenarioGlob('scenarios/**/*.yaml', testDir);
101
+ expect(files.length).toBeGreaterThan(2);
102
+ });
103
+
104
+ test('matches specific prefix pattern', async () => {
105
+ const files = await matchScenarioGlob('scenarios/auth/*.yaml', testDir);
106
+ expect(files.length).toBe(2); // login.yaml, logout.yaml
107
+ });
108
+
109
+ test('returns single file for non-glob file path', async () => {
110
+ const files = await matchScenarioGlob('scenarios/basic.yaml', testDir);
111
+ expect(files.length).toBe(1);
112
+ expect(files[0]).toContain('basic.yaml');
113
+ });
114
+
115
+ test('returns all files for non-glob directory path', async () => {
116
+ const files = await matchScenarioGlob('scenarios/auth', testDir);
117
+ expect(files.length).toBe(2);
118
+ });
119
+
120
+ test('returns empty array for non-existent path', async () => {
121
+ const files = await matchScenarioGlob('nonexistent/*.yaml', testDir);
122
+ expect(files.length).toBe(0);
123
+ });
124
+
125
+ test('matches single character with ?', async () => {
126
+ const files = await matchScenarioGlob('scenarios/api/?????.yaml', testDir);
127
+ expect(files.length).toBe(1); // users.yaml (5 chars)
128
+ });
129
+ });
130
+
131
+ describe('resolveScenarioPaths', () => {
132
+ test('resolves single file', async () => {
133
+ const files = await resolveScenarioPaths(join(testDir, 'scenarios', 'basic.yaml'));
134
+ expect(files.length).toBe(1);
135
+ });
136
+
137
+ test('resolves directory', async () => {
138
+ const files = await resolveScenarioPaths(join(testDir, 'scenarios', 'auth'));
139
+ expect(files.length).toBe(2);
140
+ });
141
+
142
+ test('resolves glob pattern', async () => {
143
+ const files = await resolveScenarioPaths('scenarios/*.yaml', testDir);
144
+ expect(files.length).toBe(2);
145
+ });
146
+
147
+ test('throws for non-existent path', async () => {
148
+ await expect(resolveScenarioPaths(join(testDir, 'nonexistent.yaml'))).rejects.toThrow(
149
+ 'not found'
150
+ );
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Scenario discovery - find scenario files by directory scanning and glob patterns
3
+ */
4
+
5
+ import { readdir, stat } from 'node:fs/promises';
6
+ import { join, resolve } from 'node:path';
7
+
8
+ /**
9
+ * Options for scenario discovery
10
+ */
11
+ export interface DiscoveryOptions {
12
+ /** File extensions to include (default: ['.yaml', '.yml']) */
13
+ extensions?: string[];
14
+ /** Maximum directory depth to scan (default: 10) */
15
+ maxDepth?: number;
16
+ /** Patterns to exclude (glob-like, e.g., 'node_modules', '*.draft.yaml') */
17
+ exclude?: string[];
18
+ }
19
+
20
+ const DEFAULT_EXTENSIONS = ['.yaml', '.yml'];
21
+ const DEFAULT_MAX_DEPTH = 10;
22
+ const DEFAULT_EXCLUDE = ['node_modules', '.git', 'dist', 'build', 'coverage'];
23
+
24
+ /**
25
+ * Check if a path matches any of the exclude patterns
26
+ */
27
+ function matchesExcludePattern(name: string, patterns: string[]): boolean {
28
+ for (const pattern of patterns) {
29
+ // Simple wildcard matching
30
+ if (pattern.includes('*')) {
31
+ const regex = new RegExp(`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`);
32
+ if (regex.test(name)) return true;
33
+ } else {
34
+ // Exact match
35
+ if (name === pattern) return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * Check if a filename has a valid scenario extension
43
+ */
44
+ function hasValidExtension(filename: string, extensions: string[]): boolean {
45
+ return extensions.some((ext) => filename.endsWith(ext));
46
+ }
47
+
48
+ /**
49
+ * Recursively scan a directory for scenario files
50
+ */
51
+ async function scanDirectoryRecursive(
52
+ dirPath: string,
53
+ options: Required<DiscoveryOptions>,
54
+ currentDepth: number
55
+ ): Promise<string[]> {
56
+ if (currentDepth > options.maxDepth) {
57
+ return [];
58
+ }
59
+
60
+ const results: string[] = [];
61
+ const entries = await readdir(dirPath, { withFileTypes: true });
62
+
63
+ for (const entry of entries) {
64
+ const fullPath = join(dirPath, entry.name);
65
+
66
+ // Skip excluded patterns
67
+ if (matchesExcludePattern(entry.name, options.exclude)) {
68
+ continue;
69
+ }
70
+
71
+ if (entry.isDirectory()) {
72
+ // Recursively scan subdirectories
73
+ const subResults = await scanDirectoryRecursive(fullPath, options, currentDepth + 1);
74
+ results.push(...subResults);
75
+ } else if (entry.isFile() && hasValidExtension(entry.name, options.extensions)) {
76
+ results.push(fullPath);
77
+ }
78
+ }
79
+
80
+ return results;
81
+ }
82
+
83
+ /**
84
+ * Discover scenario files in a directory
85
+ *
86
+ * @param dirPath - Path to the directory to scan
87
+ * @param options - Discovery options
88
+ * @returns Array of absolute paths to scenario files
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * // Scan a directory for all .yaml and .yml files
93
+ * const files = await discoverScenarios('./scenarios');
94
+ *
95
+ * // Scan with custom options
96
+ * const files = await discoverScenarios('./tests', {
97
+ * extensions: ['.yaml'],
98
+ * maxDepth: 3,
99
+ * exclude: ['drafts', '*.skip.yaml']
100
+ * });
101
+ * ```
102
+ */
103
+ export async function discoverScenarios(
104
+ dirPath: string,
105
+ options: DiscoveryOptions = {}
106
+ ): Promise<string[]> {
107
+ const resolvedPath = resolve(dirPath);
108
+ const pathStat = await stat(resolvedPath);
109
+
110
+ if (!pathStat.isDirectory()) {
111
+ throw new Error(`Path is not a directory: ${dirPath}`);
112
+ }
113
+
114
+ const fullOptions: Required<DiscoveryOptions> = {
115
+ extensions: options.extensions ?? DEFAULT_EXTENSIONS,
116
+ maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
117
+ exclude: [...DEFAULT_EXCLUDE, ...(options.exclude ?? [])],
118
+ };
119
+
120
+ const files = await scanDirectoryRecursive(resolvedPath, fullOptions, 0);
121
+
122
+ // Sort for consistent ordering
123
+ return files.sort();
124
+ }
125
+
126
+ /**
127
+ * Match scenario files using glob-like patterns
128
+ *
129
+ * Supports basic glob patterns:
130
+ * - `*` matches any characters except path separator
131
+ * - `**` matches any characters including path separator (recursive)
132
+ * - `?` matches single character
133
+ *
134
+ * @param pattern - Glob pattern to match
135
+ * @param basePath - Base path to resolve relative patterns (default: cwd)
136
+ * @returns Array of absolute paths to matching scenario files
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * // Match all yaml files in scenarios directory
141
+ * const files = await matchScenarioGlob('scenarios/*.yaml');
142
+ *
143
+ * // Match recursively
144
+ * const files = await matchScenarioGlob('tests/**\/*.yaml');
145
+ *
146
+ * // Match specific patterns
147
+ * const files = await matchScenarioGlob('scenarios/auth-*.yaml');
148
+ * ```
149
+ */
150
+ export async function matchScenarioGlob(
151
+ pattern: string,
152
+ basePath: string = process.cwd()
153
+ ): Promise<string[]> {
154
+ const resolvedBase = resolve(basePath);
155
+
156
+ // Check if the pattern contains glob characters
157
+ const hasGlob = /[*?]/.test(pattern);
158
+
159
+ if (!hasGlob) {
160
+ // Not a glob pattern - check if it's a file or directory
161
+ const fullPath = resolve(resolvedBase, pattern);
162
+ try {
163
+ const pathStat = await stat(fullPath);
164
+ if (pathStat.isFile()) {
165
+ return [fullPath];
166
+ }
167
+ if (pathStat.isDirectory()) {
168
+ return discoverScenarios(fullPath);
169
+ }
170
+ } catch {
171
+ // Path doesn't exist
172
+ return [];
173
+ }
174
+ return [];
175
+ }
176
+
177
+ // Convert glob pattern to regex
178
+ const globToRegex = (glob: string): RegExp => {
179
+ const regexStr = glob
180
+ // Escape special regex characters except * and ?
181
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
182
+ // Convert ** to match any path
183
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
184
+ // Convert * to match any characters except /
185
+ .replace(/\*/g, '[^/]*')
186
+ // Convert ? to match single character
187
+ .replace(/\?/g, '.')
188
+ // Restore ** as match-all including /
189
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*');
190
+
191
+ return new RegExp(`^${regexStr}$`);
192
+ };
193
+
194
+ // Check if pattern is an absolute path
195
+ const isAbsolute = pattern.startsWith('/');
196
+
197
+ // Extract the base directory (non-glob prefix)
198
+ const patternParts = pattern.split('/');
199
+ let baseDir = isAbsolute ? '/' : resolvedBase;
200
+ let globPart = pattern;
201
+
202
+ // Start from index 1 if absolute path (skip empty string from leading /)
203
+ const startIndex = isAbsolute ? 1 : 0;
204
+
205
+ for (let i = startIndex; i < patternParts.length; i++) {
206
+ const part = patternParts[i];
207
+ if (/[*?]/.test(part)) {
208
+ // Found first glob character - everything before is base
209
+ globPart = patternParts.slice(i).join('/');
210
+ break;
211
+ }
212
+ baseDir = join(baseDir, part);
213
+ }
214
+
215
+ // Check if base directory exists
216
+ try {
217
+ const baseStat = await stat(baseDir);
218
+ if (!baseStat.isDirectory()) {
219
+ return [];
220
+ }
221
+ } catch {
222
+ return [];
223
+ }
224
+
225
+ // Scan from base directory and filter by pattern
226
+ const allFiles = await discoverScenarios(baseDir, { maxDepth: 20 });
227
+ const regex = globToRegex(globPart);
228
+
229
+ const matchedFiles = allFiles.filter((filePath) => {
230
+ // Get relative path from base directory
231
+ const relativePath = filePath.slice(baseDir.length + 1);
232
+ return regex.test(relativePath);
233
+ });
234
+
235
+ return matchedFiles.sort();
236
+ }
237
+
238
+ /**
239
+ * Resolve a scenario path argument which can be:
240
+ * - A single file path
241
+ * - A directory path
242
+ * - A glob pattern
243
+ *
244
+ * @param pathArg - Path argument from CLI
245
+ * @param basePath - Base path for relative resolution
246
+ * @returns Array of resolved scenario file paths
247
+ */
248
+ export async function resolveScenarioPaths(
249
+ pathArg: string,
250
+ basePath: string = process.cwd()
251
+ ): Promise<string[]> {
252
+ const resolvedBase = resolve(basePath);
253
+ const hasGlob = /[*?]/.test(pathArg);
254
+
255
+ if (hasGlob) {
256
+ return matchScenarioGlob(pathArg, resolvedBase);
257
+ }
258
+
259
+ const fullPath = resolve(resolvedBase, pathArg);
260
+
261
+ try {
262
+ const pathStat = await stat(fullPath);
263
+
264
+ if (pathStat.isFile()) {
265
+ return [fullPath];
266
+ }
267
+
268
+ if (pathStat.isDirectory()) {
269
+ return discoverScenarios(fullPath);
270
+ }
271
+ } catch {
272
+ // Path doesn't exist
273
+ throw new Error(`Scenario path not found: ${pathArg}`);
274
+ }
275
+
276
+ return [];
277
+ }
@@ -5,3 +5,4 @@
5
5
  export * from './schema';
6
6
  export * from './parser';
7
7
  export * from './variables';
8
+ export * from './discovery';
@@ -42,6 +42,7 @@ export const ProviderConfigSchema = z
42
42
  resourceName: z.string().optional(),
43
43
  deploymentName: z.string().optional(),
44
44
  apiVersion: z.string().optional(),
45
+ embeddingDeploymentName: z.string().optional(),
45
46
 
46
47
  // Vercel AI specific
47
48
  underlyingProvider: z.enum(['openai', 'azure', 'anthropic', 'google', 'mistral']).optional(),
@@ -49,9 +50,9 @@ export const ProviderConfigSchema = z
49
50
  .optional();
50
51
 
51
52
  /**
52
- * Expected result types - how to evaluate responses
53
+ * Base expected types (non-recursive)
53
54
  */
54
- export const ExpectedSchema = z.discriminatedUnion('type', [
55
+ const BaseExpectedSchema = z.discriminatedUnion('type', [
55
56
  z.object({
56
57
  type: z.literal('exact'),
57
58
  value: z.string(),
@@ -84,6 +85,12 @@ export const ExpectedSchema = z.discriminatedUnion('type', [
84
85
  mode: z.enum(['all', 'any']).default('all'),
85
86
  }),
86
87
 
88
+ z.object({
89
+ type: z.literal('not_contains'),
90
+ values: z.array(z.string()),
91
+ mode: z.enum(['all', 'any']).default('all'),
92
+ }),
93
+
87
94
  z.object({
88
95
  type: z.literal('json_schema'),
89
96
  schema: z.record(z.unknown()),
@@ -94,8 +101,42 @@ export const ExpectedSchema = z.discriminatedUnion('type', [
94
101
  evaluator: z.string(),
95
102
  config: z.record(z.unknown()).optional(),
96
103
  }),
104
+
105
+ z.object({
106
+ type: z.literal('similarity'),
107
+ value: z.string(),
108
+ threshold: z.number().min(0).max(1).default(0.75),
109
+ /** Mode for similarity evaluation: 'embedding' uses vector embeddings, 'llm' uses LLM-based comparison */
110
+ mode: z.enum(['embedding', 'llm']).optional(),
111
+ /** Model for LLM-based similarity comparison (required when mode is 'llm') */
112
+ model: z.string().optional(),
113
+ /** Embedding model to use for vector similarity (required when mode is 'embedding') */
114
+ embeddingModel: z.string().optional(),
115
+ }),
116
+
117
+ z.object({
118
+ type: z.literal('inline'),
119
+ expression: z.string(),
120
+ value: z.string().optional(),
121
+ }),
97
122
  ]);
98
123
 
124
+ /**
125
+ * Combined expectation schema - allows combining multiple expectations with and/or logic
126
+ * Note: Combined expectations can only contain base expectations (no nested combined)
127
+ */
128
+ const CombinedExpectedSchema = z.object({
129
+ type: z.literal('combined'),
130
+ operator: z.enum(['and', 'or']),
131
+ expectations: z.array(BaseExpectedSchema).min(1),
132
+ });
133
+
134
+ /**
135
+ * Expected result types - how to evaluate responses
136
+ * Includes base types and combined type for logical grouping
137
+ */
138
+ export const ExpectedSchema = z.union([BaseExpectedSchema, CombinedExpectedSchema]);
139
+
99
140
  /**
100
141
  * Chat message schema
101
142
  */
@@ -2,53 +2,82 @@
2
2
  * Logger utility for Artemis
3
3
  */
4
4
 
5
- import pino from 'pino';
5
+ import { type ConsolaInstance, LogLevels, createConsola } from 'consola';
6
6
 
7
7
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
8
8
 
9
- const level = process.env.ARTEMIS_LOG_LEVEL || 'info';
9
+ const LOG_LEVEL_MAP: Record<LogLevel, number> = {
10
+ debug: LogLevels.debug,
11
+ info: LogLevels.info,
12
+ warn: LogLevels.warn,
13
+ error: LogLevels.error,
14
+ };
10
15
 
11
- const baseLogger = pino({
12
- level,
13
- transport:
14
- process.env.NODE_ENV === 'development'
15
- ? { target: 'pino-pretty', options: { colorize: true } }
16
- : undefined,
16
+ const level = (process.env.ARTEMIS_LOG_LEVEL as LogLevel) || 'info';
17
+
18
+ const baseLogger = createConsola({
19
+ level: LOG_LEVEL_MAP[level] ?? LogLevels.info,
20
+ formatOptions: {
21
+ colors: true,
22
+ date: true,
23
+ },
17
24
  });
18
25
 
19
26
  /**
20
27
  * Logger class for consistent logging across Artemis
21
28
  */
22
29
  export class Logger {
23
- private logger: pino.Logger;
30
+ private logger: ConsolaInstance;
24
31
 
25
32
  constructor(name: string) {
26
- this.logger = baseLogger.child({ name });
33
+ this.logger = baseLogger.withTag(name);
27
34
  }
28
35
 
29
36
  debug(message: string, data?: Record<string, unknown>): void {
30
- this.logger.debug(data, message);
37
+ if (data) {
38
+ this.logger.debug(message, data);
39
+ } else {
40
+ this.logger.debug(message);
41
+ }
31
42
  }
32
43
 
33
44
  info(message: string, data?: Record<string, unknown>): void {
34
- this.logger.info(data, message);
45
+ if (data) {
46
+ this.logger.info(message, data);
47
+ } else {
48
+ this.logger.info(message);
49
+ }
35
50
  }
36
51
 
37
52
  warn(message: string, data?: Record<string, unknown>): void {
38
- this.logger.warn(data, message);
53
+ if (data) {
54
+ this.logger.warn(message, data);
55
+ } else {
56
+ this.logger.warn(message);
57
+ }
39
58
  }
40
59
 
41
60
  error(message: string, error?: Error | unknown, data?: Record<string, unknown>): void {
42
61
  const errorData =
43
62
  error instanceof Error
44
63
  ? { error: { message: error.message, stack: error.stack, name: error.name } }
45
- : { error };
46
- this.logger.error({ ...data, ...errorData }, message);
64
+ : error
65
+ ? { error }
66
+ : undefined;
67
+
68
+ const mergedData = errorData || data ? { ...data, ...errorData } : undefined;
69
+
70
+ if (mergedData) {
71
+ this.logger.error(message, mergedData);
72
+ } else {
73
+ this.logger.error(message);
74
+ }
47
75
  }
48
76
 
49
77
  child(bindings: Record<string, unknown>): Logger {
50
78
  const childLogger = new Logger('');
51
- childLogger.logger = this.logger.child(bindings);
79
+ const tag = bindings.name ? String(bindings.name) : '';
80
+ childLogger.logger = this.logger.withTag(tag);
52
81
  return childLogger;
53
82
  }
54
83
  }