Skip to content

Commit 2a839b5

Browse files
authored
Merge pull request #224 from objectstack-ai/copilot/improve-logging-structure
2 parents e498128 + f099997 commit 2a839b5

230 files changed

Lines changed: 2815 additions & 1042 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

DEVELOPMENT_WORKFLOW.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,68 @@ pnpm typecheck
303303
pnpm test
304304
```
305305

306+
## 📝 Logging
307+
308+
HotCRM uses **structured logging** via [pino](https://getpino.io). All log output is JSON, making it easy to search, filter, and aggregate in production.
309+
310+
### Creating a Logger
311+
312+
```typescript
313+
import { createLogger } from '@hotcrm/core';
314+
315+
const logger = createLogger('crm:account');
316+
```
317+
318+
The module name follows the convention `package:module` (e.g. `crm:account`, `finance:invoice`, `ai:cache-manager`).
319+
320+
### Log Levels
321+
322+
| Level | Method | When to use |
323+
|-------|--------|-------------|
324+
| `debug` | `logger.debug(...)` | Verbose development-time tracing |
325+
| `info` | `logger.info(...)` | Normal operational events |
326+
| `warn` | `logger.warn(...)` | Something unexpected but recoverable |
327+
| `error` | `logger.error(...)` | Failures that need attention |
328+
329+
### Structured Data
330+
331+
Always pass structured data as the **first argument** (object), and a short message string as the **second**:
332+
333+
```typescript
334+
// Good — structured data is machine-parseable
335+
logger.info({ accountId, healthScore: 85 }, 'Account scored');
336+
logger.error({ err }, 'Failed to update account');
337+
338+
// Bad — don't embed data in the message string
339+
logger.info(`Account ${accountId} scored 85`);
340+
```
341+
342+
### Controlling Log Level
343+
344+
Set the `LOG_LEVEL` environment variable:
345+
346+
```bash
347+
LOG_LEVEL=debug pnpm dev # See all log output
348+
LOG_LEVEL=warn pnpm start # Only warnings and errors in production
349+
```
350+
351+
### Testing with Logger
352+
353+
In tests, mock `@hotcrm/core` so log calls delegate to `console.*` (allowing existing `vi.spyOn(console, ...)` patterns):
354+
355+
```typescript
356+
vi.mock('@hotcrm/core', () => ({
357+
createLogger: () => ({
358+
info: (...args: any[]) => console.log(...args),
359+
warn: (...args: any[]) => console.warn(...args),
360+
error: (...args: any[]) => console.error(...args),
361+
debug: (...args: any[]) => console.debug(...args),
362+
}),
363+
}));
364+
```
365+
366+
> **Note:** Do not use `console.log` directly in production source code. Always use `createLogger` from `@hotcrm/core`.
367+
306368
## ❓ Troubleshooting FAQ
307369

308370
### `pnpm install` fails

ROADMAP.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
| Studio Builder Configs | 4 (Interface Builder, Page Builder, Canvas Snap, Element Palette) |
6666
| Navigation Areas | 3 (header, sidebar, utility bar) |
6767
| Packages Registered in Root Config | 13 of 13 (all packages registered) |
68-
| Test Files | 192 files, 3799 tests (all passing) |
68+
| Test Files | 194 files, 3813 tests (all passing) |
6969
| TypeScript Compliance | 100% (zero type errors) |
7070
| Protocol Compliance | 100% (all objects pass ObjectSchema.create()) |
7171
| Spec Schema Adoption | ~95 of ~95 application-level schemas used (~100%) |
@@ -193,8 +193,10 @@ These items were deferred during Phase 13 and have been completed:
193193

194194
#### Deferred / Future
195195

196-
- [ ] **Structured logging migration** — Replace `console.log` with pino logger across hook files
197-
- [ ] **Legacy API cleanup** — Remove `db.ts` references and migrate to broker/ObjectQL
196+
- [x] **Structured logging migration** — Replaced `console.log` with pino logger (`createLogger` from `@hotcrm/core`) across 154 hook/action/service files in all 13 packages; removed emoji from log output; added 9 logger unit tests
197+
- [x] **CacheManager test isolation** — Added `CacheManager.resetInstance()` for test isolation; 5 dedicated tests
198+
- [x] **Legacy API cleanup (Phase 1)** — Added `@deprecated` annotations and migration guide to all 13 `db.ts` files; `db` export marked for removal
199+
- [ ] **Legacy API cleanup (Phase 2)** — Remove `db.ts` exports entirely once all consumers migrated to broker/ObjectQL
198200
- [ ] **E2E test coverage** — Build API-layer end-to-end tests for Feed API / MCP API
199201
- [ ] **FormView `as any` cleanup** — Remove `as any` casts once `@objectstack/spec` aligns FormView column types
200202

@@ -204,6 +206,7 @@ These items were deferred during Phase 13 and have been completed:
204206

205207
| Date | From | To | Breaking Changes | Tests |
206208
|------|------|----|-----------------|-------|
209+
| 2026-02-21 | v3.0.8 | v3.0.8 | None (Phase 15 continued: Structured pino logging across 154 files, CacheManager test isolation, db.ts deprecation annotations, emoji removal from logs) | 3813 ✅ |
207210
| 2026-02-21 | v3.0.8 | v3.0.8 | None (Phase 15: Repository quality improvements — hook `any` type migration, CI typecheck, ESLint version alignment, documentation updates, code-quality workflow fixes) | 3799 ✅ |
208211
| 2027-02-21 | v3.0.8 | v3.0.8 | None (Phase 14 complete: Activity Feed/Chatter across 6 clouds, 4 Interface Builder blank pages, Dashboard headers & global filters on 6 clouds, ViewTabs on 18 views, Feed API 8 endpoints, Feed service contract, System metadata, Studio builder configs, Navigation areas, 327 new tests) | 3707 ✅ |
209212
| 2026-02-21 | v3.0.6 | v3.0.8 | None (New: Activity Feed/Chatter system, Interface Builder/Blank Pages, Dashboard headers & global filters, Feed API contracts, Studio builder configs, Oclif CLI plugin, Package conventions; Phase 14 roadmap added) | 3318 ✅ |
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { CacheManager } from '../../src/cache-manager';
3+
4+
describe('CacheManager — resetInstance test isolation', () => {
5+
afterEach(() => {
6+
CacheManager.resetInstance();
7+
});
8+
9+
it('should fully reset singleton so next getInstance creates fresh instance', async () => {
10+
const cm1 = CacheManager.getInstance();
11+
await cm1.set('key', 'value', 60);
12+
expect(await cm1.get('key')).toBe('value');
13+
14+
CacheManager.resetInstance();
15+
16+
const cm2 = CacheManager.getInstance();
17+
// After reset, the new instance should have an empty cache
18+
expect(await cm2.get('key')).toBeNull();
19+
expect(cm2.getStats().size).toBe(0);
20+
});
21+
22+
it('should allow a different config after reset', () => {
23+
const cm1 = CacheManager.getInstance({ defaultTtl: 300 });
24+
CacheManager.resetInstance();
25+
26+
const cm2 = CacheManager.getInstance({ defaultTtl: 600 });
27+
// Should be a brand new instance (not same reference as cm1)
28+
expect(cm2).not.toBe(cm1);
29+
});
30+
31+
it('should not throw when called before any getInstance', () => {
32+
// Fresh state — no instance exists yet
33+
expect(() => CacheManager.resetInstance()).not.toThrow();
34+
});
35+
36+
it('should not throw when called twice in a row', () => {
37+
CacheManager.getInstance();
38+
CacheManager.resetInstance();
39+
expect(() => CacheManager.resetInstance()).not.toThrow();
40+
});
41+
42+
it('should isolate cache state between test runs', async () => {
43+
// Simulate test A
44+
const cmA = CacheManager.getInstance();
45+
await cmA.set('shared', 'from-test-a', 60);
46+
CacheManager.resetInstance();
47+
48+
// Simulate test B — should not see test A's data
49+
const cmB = CacheManager.getInstance();
50+
expect(await cmB.get('shared')).toBeNull();
51+
});
52+
});

packages/ai/src/benchmark.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
* Usage:
88
* const bench = new Benchmark('lead_scoring');
99
* const result = await bench.run(() => scoreLead(lead), { iterations: 100 });
10-
* console.log(result.summary());
10+
* logger.info(result.summary());
1111
*/
1212

13+
import { createLogger } from '@hotcrm/core';
14+
const logger = createLogger('ai:benchmark');
15+
1316
// ---------------------------------------------------------------------------
1417
// Types
1518
// ---------------------------------------------------------------------------

packages/ai/src/cache-manager.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* Falls back to in-memory cache if Redis is not available
66
*/
77

8+
import { createLogger } from '@hotcrm/core';
9+
const logger = createLogger('ai:cache-manager');
10+
811
export interface CacheConfig {
912
/** Redis connection URL */
1013
redisUrl?: string;
@@ -57,12 +60,25 @@ export class CacheManager {
5760
return CacheManager.instance;
5861
}
5962

63+
/**
64+
* Reset singleton instance for test isolation.
65+
* Call in beforeEach/afterEach to ensure a clean slate between tests.
66+
*/
67+
static resetInstance(): void {
68+
if (CacheManager.instance) {
69+
CacheManager.instance.memoryCache.clear();
70+
CacheManager.instance.redisClient = null;
71+
CacheManager.instance.useRedis = false;
72+
}
73+
CacheManager.instance = undefined as unknown as CacheManager;
74+
}
75+
6076
/**
6177
* Initialize Redis client
6278
*/
6379
private async initializeRedis(): Promise<void> {
6480
if (!this.config.redisUrl) {
65-
console.log('[Cache] No Redis URL provided, using in-memory cache');
81+
logger.info('[Cache] No Redis URL provided, using in-memory cache');
6682
return;
6783
}
6884

@@ -72,14 +88,14 @@ export class CacheManager {
7288
// this.redisClient = createClient({ url: this.config.redisUrl });
7389
// await this.redisClient.connect();
7490
// this.useRedis = true;
75-
// console.log('[Cache] Redis connected successfully');
91+
// logger.info('[Cache] Redis connected successfully');
7692

77-
console.log('[Cache] Redis initialization placeholder - using in-memory cache');
93+
logger.info('[Cache] Redis initialization placeholder - using in-memory cache');
7894
} catch (error) {
79-
console.error('[Cache] Redis initialization failed:', error);
95+
logger.error('[Cache] Redis initialization failed:', error);
8096

8197
if (this.config.useMemoryFallback) {
82-
console.log('[Cache] Falling back to in-memory cache');
98+
logger.info('[Cache] Falling back to in-memory cache');
8399
}
84100
}
85101
}
@@ -120,7 +136,7 @@ export class CacheManager {
120136

121137
return entry.data as T;
122138
} catch (error) {
123-
console.error('[Cache] Get error:', error);
139+
logger.error('[Cache] Get error:', error);
124140
return null;
125141
}
126142
}
@@ -153,7 +169,7 @@ export class CacheManager {
153169
// Clean up expired entries periodically
154170
this.cleanupExpired();
155171
} catch (error) {
156-
console.error('[Cache] Set error:', error);
172+
logger.error('[Cache] Set error:', error);
157173
}
158174
}
159175

@@ -169,7 +185,7 @@ export class CacheManager {
169185

170186
this.memoryCache.delete(key);
171187
} catch (error) {
172-
console.error('[Cache] Delete error:', error);
188+
logger.error('[Cache] Delete error:', error);
173189
}
174190
}
175191

@@ -185,7 +201,7 @@ export class CacheManager {
185201

186202
this.memoryCache.clear();
187203
} catch (error) {
188-
console.error('[Cache] Clear error:', error);
204+
logger.error('[Cache] Clear error:', error);
189205
}
190206
}
191207

packages/ai/src/prediction-service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { BaseMLProvider } from './providers/base-provider.js';
1111
import CacheManager from './cache-manager.js';
1212
import PerformanceMonitor from './performance-monitor.js';
1313

14+
import { createLogger } from '@hotcrm/core';
15+
const logger = createLogger('ai:prediction-service');
16+
1417
export interface PredictionRequest {
1518
/** Model ID to use */
1619
modelId: string;
@@ -191,7 +194,7 @@ export class PredictionService {
191194
metadata: result.metadata
192195
}));
193196
} catch (error) {
194-
console.warn('[PredictionService] Batch prediction failed, falling back to sequential:', error);
197+
logger.warn('[PredictionService] Batch prediction failed, falling back to sequential:', error);
195198
}
196199
}
197200

@@ -238,7 +241,7 @@ export class PredictionService {
238241
const result = await provider.predict<T>(model.id, { features, context });
239242
return result;
240243
} catch (error) {
241-
console.warn(`[PredictionService] Provider prediction failed for ${model.id}, falling back to mock:`, error);
244+
logger.warn(`[PredictionService] Provider prediction failed for ${model.id}, falling back to mock:`, error);
242245
}
243246
}
244247

packages/ai/src/providers/aws-sagemaker-provider.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
import BaseMLProvider, { MLProviderConfig, PredictionInput, PredictionOutput } from './base-provider.js';
88

9+
import { createLogger } from '@hotcrm/core';
10+
const logger = createLogger('ai:aws-sagemaker-provider');
11+
912
export interface SageMakerConfig extends MLProviderConfig {
1013
provider: 'aws-sagemaker';
1114
credentials: {
@@ -46,7 +49,7 @@ export class AWSSageMakerProvider extends BaseMLProvider {
4649
this.client = {
4750
send: async (command: any) => {
4851
// Mock implementation
49-
console.log('[SageMaker] Would invoke endpoint:', command.input?.EndpointName);
52+
logger.info('[SageMaker] Would invoke endpoint:', command.input?.EndpointName);
5053
const mockBody = JSON.stringify({ predictions: [0.85] });
5154
return { Body: { toString: () => mockBody } };
5255
}
@@ -66,7 +69,7 @@ export class AWSSageMakerProvider extends BaseMLProvider {
6669

6770
return true;
6871
} catch (error) {
69-
console.error('[SageMaker] Validation failed:', error);
72+
logger.error('[SageMaker] Validation failed:', error);
7073
return false;
7174
}
7275
}
@@ -112,7 +115,7 @@ export class AWSSageMakerProvider extends BaseMLProvider {
112115
}
113116
};
114117
} catch (error) {
115-
console.error('[SageMaker] Prediction error:', error);
118+
logger.error('[SageMaker] Prediction error:', error);
116119
throw new Error(`SageMaker prediction failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
117120
}
118121
}

packages/ai/src/providers/azure-ml-provider.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
import BaseMLProvider, { MLProviderConfig, PredictionInput, PredictionOutput } from './base-provider.js';
88

9+
import { createLogger } from '@hotcrm/core';
10+
const logger = createLogger('ai:azure-ml-provider');
11+
912
// Type stubs for when axios is not available
1013
interface AxiosInstance {
1114
post(url: string, data: any): Promise<any>;
@@ -83,7 +86,7 @@ export class AzureMLProvider extends BaseMLProvider {
8386

8487
return true;
8588
} catch (error) {
86-
console.error('[Azure ML] Validation failed:', error);
89+
logger.error('[Azure ML] Validation failed:', error);
8790
return false;
8891
}
8992
}
@@ -128,7 +131,7 @@ export class AzureMLProvider extends BaseMLProvider {
128131
}
129132
};
130133
} catch (error) {
131-
console.error('[Azure ML] Prediction error:', error);
134+
logger.error('[Azure ML] Prediction error:', error);
132135

133136
const err = error as any;
134137
if (err.isAxiosError) {
@@ -175,7 +178,7 @@ export class AzureMLProvider extends BaseMLProvider {
175178
}
176179
}));
177180
} catch (error) {
178-
console.error('[Azure ML] Batch prediction error:', error);
181+
logger.error('[Azure ML] Batch prediction error:', error);
179182
throw new Error(`Azure ML batch prediction failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
180183
}
181184
}

packages/ai/src/providers/openai-provider.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
import BaseMLProvider, { MLProviderConfig, PredictionInput, PredictionOutput } from './base-provider.js';
88

9+
import { createLogger } from '@hotcrm/core';
10+
const logger = createLogger('ai:openai-provider');
11+
912
// Type stubs for when axios is not available
1013
interface AxiosInstance {
1114
post(url: string, data: any): Promise<any>;
@@ -86,7 +89,7 @@ export class OpenAIProvider extends BaseMLProvider {
8689

8790
return true;
8891
} catch (error) {
89-
console.error('[OpenAI] Validation failed:', error);
92+
logger.error('[OpenAI] Validation failed:', error);
9093
return false;
9194
}
9295
}
@@ -128,7 +131,7 @@ export class OpenAIProvider extends BaseMLProvider {
128131
}
129132
};
130133
} catch (error) {
131-
console.error('[OpenAI] Prediction error:', error);
134+
logger.error('[OpenAI] Prediction error:', error);
132135

133136
const err = error as any;
134137
if (err.isAxiosError) {

0 commit comments

Comments
 (0)