mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(freight-api): enrich canonical request log line
Emit the request line as raw JSON on stdout (level/time/logger as fields) instead of through Nest's console logger, whose prefix made it unparsable. Collect data points via logCtx at the flow chokepoints: BaseRepository writes (status changes, creates, deletes), invoice transitions, payment intent lifecycle + outbound payment-service calls, booking/contract entry state, review-note reasons, signatures and OTP verify outcomes.
This commit is contained in:
@@ -59,7 +59,6 @@ const userId = (req: LoggedRequest): string | undefined => {
|
||||
@Injectable()
|
||||
export class RequestLogMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger("HTTP");
|
||||
private readonly canonical = new Logger("request");
|
||||
|
||||
use(req: LoggedRequest, res: LoggedResponse, next: () => void): void {
|
||||
const start = Date.now();
|
||||
@@ -90,6 +89,12 @@ export class RequestLogMiddleware implements NestMiddleware {
|
||||
|
||||
const line = {
|
||||
...ctx,
|
||||
// Fields the Nest console prefix used to carry. They are IN the JSON
|
||||
// now because this line is written raw (see below) — a log shipper
|
||||
// needs level and time as parsable fields, not as console decoration.
|
||||
time: new Date().toISOString(),
|
||||
level: status >= 500 ? "error" : status >= 400 ? "warn" : "info",
|
||||
logger: "request",
|
||||
type: "http_request",
|
||||
requestId,
|
||||
method: req.method,
|
||||
@@ -113,6 +118,9 @@ export class RequestLogMiddleware implements NestMiddleware {
|
||||
json = JSON.stringify(line);
|
||||
} catch {
|
||||
json = JSON.stringify({
|
||||
time: line.time,
|
||||
level: line.level,
|
||||
logger: "request",
|
||||
type: "http_request",
|
||||
requestId,
|
||||
method: req.method,
|
||||
@@ -123,9 +131,11 @@ export class RequestLogMiddleware implements NestMiddleware {
|
||||
});
|
||||
}
|
||||
|
||||
if (status >= 500) this.canonical.error(json);
|
||||
else if (status >= 400) this.canonical.warn(json);
|
||||
else this.canonical.log(json);
|
||||
// Written raw, NOT through Nest's Logger: the console logger wraps every
|
||||
// message in "[Nest] pid - date LEVEL [ctx] …", which makes the line
|
||||
// un-parsable as JSON. Same destination the Nest logger writes to
|
||||
// (stdout, stderr for errors) — only the decoration is dropped.
|
||||
(status >= 500 ? process.stderr : process.stdout).write(`${json}\n`);
|
||||
};
|
||||
|
||||
res.on("finish", emit);
|
||||
|
||||
@@ -7,9 +7,16 @@ import {
|
||||
Repository,
|
||||
} from "typeorm";
|
||||
|
||||
import { logCtx } from "../logging/request-context";
|
||||
|
||||
export abstract class BaseRepository<T extends ObjectLiteral> {
|
||||
protected constructor(protected readonly repository: Repository<T>) {}
|
||||
|
||||
/** Table name, for the write trail on the canonical request log line. */
|
||||
private get table(): string {
|
||||
return this.repository.metadata.tableName;
|
||||
}
|
||||
|
||||
/** Find a single entity by its primary key. */
|
||||
async findById(
|
||||
id: string,
|
||||
@@ -34,22 +41,43 @@ export abstract class BaseRepository<T extends ObjectLiteral> {
|
||||
/** Create and persist a new entity. */
|
||||
async create(data: DeepPartial<T>): Promise<T> {
|
||||
const entity = this.repository.create(data);
|
||||
return this.repository.save(entity);
|
||||
const saved = await this.repository.save(entity);
|
||||
logCtx(1, { path: `db.created.${this.table}`, mode: "count" });
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** Patch an entity in place and return the reloaded row. */
|
||||
/**
|
||||
* Patch an entity in place and return the reloaded row.
|
||||
*
|
||||
* Every domain status machine in this app (booking, contract, wagon,
|
||||
* warehouse, transfer request…) lands here, so this is the one place that can
|
||||
* record "what state did this request actually move" without every service
|
||||
* remembering to log it.
|
||||
*/
|
||||
async update(id: string, data: DeepPartial<T>): Promise<T | null> {
|
||||
await this.repository.update(id, data as never);
|
||||
logCtx(1, { path: `db.updated.${this.table}`, mode: "count" });
|
||||
if (data && typeof data === "object" && "status" in data) {
|
||||
logCtx(
|
||||
{ entity: this.table, id, status: (data as { status: unknown }).status },
|
||||
{ path: "statusChanges", mode: "push" },
|
||||
);
|
||||
}
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/** Soft-delete an entity by primary key (sets deleted_at). */
|
||||
async softDelete(id: string): Promise<void> {
|
||||
await this.repository.softDelete(id);
|
||||
logCtx({ entity: this.table, id }, { path: "deleted", mode: "push" });
|
||||
}
|
||||
|
||||
/** Permanently delete an entity. Avoid in domain code; prefer softDelete. */
|
||||
async hardDelete(id: string): Promise<void> {
|
||||
await this.repository.delete(id);
|
||||
logCtx(
|
||||
{ entity: this.table, id, hard: true },
|
||||
{ path: "deleted", mode: "push" },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user