feat(api): Protokollzeile je Anfrage und je Nextcloud-Pruefung im Docker-Log
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { formatRequestLine } from './request-log';
|
||||||
|
|
||||||
|
describe('formatRequestLine', () => {
|
||||||
|
it('schreibt Methode, Pfad, Status, Dauer und Benutzer', () => {
|
||||||
|
expect(
|
||||||
|
formatRequestLine({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/modules/x',
|
||||||
|
status: 200,
|
||||||
|
ms: 12,
|
||||||
|
username: 'admin',
|
||||||
|
}),
|
||||||
|
).toBe('GET /modules/x 200 12 ms user=admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('laesst den Abfrageteil weg', () => {
|
||||||
|
expect(
|
||||||
|
formatRequestLine({ method: 'GET', url: '/auth/reset?token=geheim', status: 200, ms: 1 }),
|
||||||
|
).toBe('GET /auth/reset 200 1 ms user=-');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('markiert langsame Anfragen', () => {
|
||||||
|
expect(formatRequestLine({ method: 'POST', url: '/a', status: 201, ms: 4500 })).toContain(
|
||||||
|
'LANGSAM',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Protokollzeile je API-Anfrage im Docker-Log (quick-261002):
|
||||||
|
* `GET /modules/nextcloud-status/instances 200 34 ms user=admin`.
|
||||||
|
*
|
||||||
|
* Nur Methode, Pfad OHNE Abfrageteil (dort koennten Kennungen oder Token
|
||||||
|
* stehen), Status, Dauer und Benutzername — nie Koerper, Cookies oder
|
||||||
|
* Kopfzeilen. `/health` wird ausgelassen, der Docker-Healthcheck fragt es
|
||||||
|
* alle paar Sekunden ab. Fehler (>= 500) als `error`, Abweisungen (>= 400)
|
||||||
|
* als `warn`, langsame Anfragen (>= 3 s) mit Vermerk.
|
||||||
|
*/
|
||||||
|
const logger = new Logger('HTTP');
|
||||||
|
export const SLOW_REQUEST_MS = 3000;
|
||||||
|
|
||||||
|
export function formatRequestLine(input: {
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
status: number;
|
||||||
|
ms: number;
|
||||||
|
username?: string | null;
|
||||||
|
}): string {
|
||||||
|
const path = input.url.split('?')[0] ?? input.url;
|
||||||
|
const slow = input.ms >= SLOW_REQUEST_MS ? ' LANGSAM' : '';
|
||||||
|
return `${input.method} ${path} ${input.status} ${input.ms} ms user=${input.username ?? '-'}${slow}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestLogMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||||
|
const url = req.originalUrl ?? req.url;
|
||||||
|
if (url === '/health' || url.startsWith('/health?') || url.startsWith('/health/')) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const started = process.hrtime.bigint();
|
||||||
|
res.on('finish', () => {
|
||||||
|
const ms = Number((process.hrtime.bigint() - started) / 1_000_000n);
|
||||||
|
const username = (req as Request & { user?: { username?: string } }).user?.username;
|
||||||
|
const line = formatRequestLine({
|
||||||
|
method: req.method,
|
||||||
|
url,
|
||||||
|
status: res.statusCode,
|
||||||
|
ms,
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
if (res.statusCode >= 500) logger.error(line);
|
||||||
|
else if (res.statusCode >= 400 || ms >= SLOW_REQUEST_MS) logger.warn(line);
|
||||||
|
else logger.log(line);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
@@ -3,12 +3,16 @@ import { ConfigService } from '@nestjs/config';
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { requestLogMiddleware } from './common/request-log';
|
||||||
import { formatAppVersionLine } from './health/app-version';
|
import { formatAppVersionLine } from './health/app-version';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
const configService = app.get(ConfigService);
|
const configService = app.get(ConfigService);
|
||||||
|
|
||||||
|
// Eine Zeile je Anfrage im Docker-Log (Pfad, Status, Dauer, Benutzer)
|
||||||
|
app.use(requestLogMiddleware);
|
||||||
|
|
||||||
// Cookie parser for JWT httpOnly cookies
|
// Cookie parser for JWT httpOnly cookies
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
@@ -21,10 +25,7 @@ async function bootstrap() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// CORS with credentials for cross-origin cookie support (Pitfall 4)
|
// CORS with credentials for cross-origin cookie support (Pitfall 4)
|
||||||
const corsOrigin = configService.get<string>(
|
const corsOrigin = configService.get<string>('CORS_ORIGIN', 'http://localhost:3000');
|
||||||
'CORS_ORIGIN',
|
|
||||||
'http://localhost:3000',
|
|
||||||
);
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: corsOrigin,
|
origin: corsOrigin,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ export class NextcloudStatusSchedulerService implements OnApplicationBootstrap {
|
|||||||
for (const [tenantId, ids] of byTenant) {
|
for (const [tenantId, ids] of byTenant) {
|
||||||
for (const id of ids) work.push({ tenantId, id });
|
for (const id of ids) work.push({ tenantId, id });
|
||||||
}
|
}
|
||||||
|
if (work.length === 0) return;
|
||||||
|
const startedAt = Date.now();
|
||||||
await runWithConcurrency(work, CHECK_CONCURRENCY, async ({ tenantId, id }) => {
|
await runWithConcurrency(work, CHECK_CONCURRENCY, async ({ tenantId, id }) => {
|
||||||
try {
|
try {
|
||||||
await this.service.checkInstance(tenantId, id);
|
await this.service.checkInstance(tenantId, id);
|
||||||
@@ -155,5 +157,8 @@ export class NextcloudStatusSchedulerService implements OnApplicationBootstrap {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.logger.log(
|
||||||
|
`Nextcloud-Pruefdurchlauf: ${work.length} Cloud(s) in ${Date.now() - startedAt} ms`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export class NextcloudStatusService {
|
|||||||
});
|
});
|
||||||
if (!existing) throw new NotFoundException('Cloud nicht gefunden');
|
if (!existing) throw new NotFoundException('Cloud nicht gefunden');
|
||||||
|
|
||||||
|
const startedAt = Date.now();
|
||||||
const result = await fetchNextcloudStatus(existing.baseUrl);
|
const result = await fetchNextcloudStatus(existing.baseUrl);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const plan = planStatusWrite(existing.consecutiveFailures, result, now);
|
const plan = planStatusWrite(existing.consecutiveFailures, result, now);
|
||||||
@@ -228,6 +229,12 @@ export class NextcloudStatusService {
|
|||||||
select: { ...PUBLIC_SELECT, alertState: true },
|
select: { ...PUBLIC_SELECT, alertState: true },
|
||||||
});
|
});
|
||||||
const view = this.toView(updated as PublicRow, await this.release.getReference());
|
const view = this.toView(updated as PublicRow, await this.release.getReference());
|
||||||
|
const outcome = result.reachable
|
||||||
|
? `Version ${result.versionString ?? '?'}${result.maintenance ? ', Wartungsmodus' : ''}`
|
||||||
|
: `Fehler ${result.errorKind}${result.errorDetail ? ` (${result.errorDetail})` : ''}`;
|
||||||
|
this.logger.log(
|
||||||
|
`Pruefung "${updated.customerName}" ${existing.baseUrl}: ${outcome}, Ampel ${view.rating.level}, ${Date.now() - startedAt} ms`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await this.alerts.evaluateAfterCheck(
|
await this.alerts.evaluateAfterCheck(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
Reference in New Issue
Block a user