feat(P4-003): @mosaic/log — log ingest, parsing, tiered storage

Implement AgentLogsRepo with structured log ingest (single + batch),
flexible query builder (filter by session, level, category, tier,
date range), and tiered storage management (hot→warm→cold→purge).
Add getLogsForSummarization() for the summarization pipeline.
Wire LogModule into gateway with REST endpoints at /api/logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 08:51:10 -05:00
parent fb3c308efd
commit 666d2bc36d
12 changed files with 260 additions and 2 deletions

View File

@@ -18,6 +18,7 @@
"@mosaic/brain": "workspace:^",
"@mosaic/coord": "workspace:^",
"@mosaic/db": "workspace:^",
"@mosaic/log": "workspace:^",
"@mosaic/memory": "workspace:^",
"@mosaic/types": "workspace:^",
"@nestjs/common": "^11.0.0",

View File

@@ -11,6 +11,7 @@ import { MissionsModule } from './missions/missions.module.js';
import { TasksModule } from './tasks/tasks.module.js';
import { CoordModule } from './coord/coord.module.js';
import { MemoryModule } from './memory/memory.module.js';
import { LogModule } from './log/log.module.js';
@Module({
imports: [
@@ -25,6 +26,7 @@ import { MemoryModule } from './memory/memory.module.js';
TasksModule,
CoordModule,
MemoryModule,
LogModule,
],
controllers: [HealthController],
})

View File

@@ -0,0 +1,62 @@
import { Body, Controller, Get, Inject, Param, Post, Query, UseGuards } from '@nestjs/common';
import type { LogService } from '@mosaic/log';
import { LOG_SERVICE } from './log.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import type { IngestLogDto, QueryLogsDto } from './log.dto.js';
@Controller('api/logs')
@UseGuards(AuthGuard)
export class LogController {
constructor(@Inject(LOG_SERVICE) private readonly logService: LogService) {}
@Post()
async ingest(@Query('userId') userId: string, @Body() dto: IngestLogDto) {
return this.logService.logs.ingest({
sessionId: dto.sessionId,
userId,
level: dto.level,
category: dto.category,
content: dto.content,
metadata: dto.metadata,
});
}
@Post('batch')
async ingestBatch(@Query('userId') userId: string, @Body() dtos: IngestLogDto[]) {
const entries = dtos.map((dto) => ({
sessionId: dto.sessionId,
userId,
level: dto.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
category: dto.category as
| 'decision'
| 'tool_use'
| 'learning'
| 'error'
| 'general'
| undefined,
content: dto.content,
metadata: dto.metadata,
}));
return this.logService.logs.ingestBatch(entries);
}
@Get()
async query(@Query('userId') userId: string, @Query() params: QueryLogsDto) {
return this.logService.logs.query({
userId,
sessionId: params.sessionId,
level: params.level,
category: params.category,
tier: params.tier,
since: params.since ? new Date(params.since) : undefined,
until: params.until ? new Date(params.until) : undefined,
limit: params.limit ? Number(params.limit) : undefined,
offset: params.offset ? Number(params.offset) : undefined,
});
}
@Get(':id')
async findOne(@Param('id') id: string) {
return this.logService.logs.findById(id);
}
}

View File

@@ -0,0 +1,18 @@
export interface IngestLogDto {
sessionId: string;
level?: 'debug' | 'info' | 'warn' | 'error';
category?: 'decision' | 'tool_use' | 'learning' | 'error' | 'general';
content: string;
metadata?: Record<string, unknown>;
}
export interface QueryLogsDto {
sessionId?: string;
level?: 'debug' | 'info' | 'warn' | 'error';
category?: 'decision' | 'tool_use' | 'learning' | 'error' | 'general';
tier?: 'hot' | 'warm' | 'cold';
since?: string;
until?: string;
limit?: string;
offset?: string;
}

View File

@@ -0,0 +1,20 @@
import { Global, Module } from '@nestjs/common';
import { createLogService, type LogService } from '@mosaic/log';
import type { Db } from '@mosaic/db';
import { DB } from '../database/database.module.js';
import { LOG_SERVICE } from './log.tokens.js';
import { LogController } from './log.controller.js';
@Global()
@Module({
providers: [
{
provide: LOG_SERVICE,
useFactory: (db: Db): LogService => createLogService(db),
inject: [DB],
},
],
controllers: [LogController],
exports: [LOG_SERVICE],
})
export class LogModule {}

View File

@@ -0,0 +1 @@
export const LOG_SERVICE = 'LOG_SERVICE';