0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

feat(core): koa-log middleware (#590)

This commit is contained in:
IceHe.xyz 2022-04-20 14:56:33 +08:00 committed by GitHub
parent a0af0584f7
commit 4491eab5b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 2 deletions

View file

@ -2,6 +2,7 @@ import Koa from 'koa';
import * as koaErrorHandler from '@/middleware/koa-error-handler'; import * as koaErrorHandler from '@/middleware/koa-error-handler';
import * as koaI18next from '@/middleware/koa-i18next'; import * as koaI18next from '@/middleware/koa-i18next';
import * as koaLog from '@/middleware/koa-log';
import * as koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler'; import * as koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
import * as koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler'; import * as koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
import * as koaSpaProxy from '@/middleware/koa-spa-proxy'; import * as koaSpaProxy from '@/middleware/koa-spa-proxy';
@ -18,6 +19,7 @@ describe('App Init', () => {
const middlewareList = [ const middlewareList = [
koaErrorHandler, koaErrorHandler,
koaI18next, koaI18next,
koaLog,
koaOIDCErrorHandler, koaOIDCErrorHandler,
koaSlonikErrorHandler, koaSlonikErrorHandler,
koaSpaProxy, koaSpaProxy,

View file

@ -9,6 +9,7 @@ import envSet, { MountedApps } from '@/env-set';
import koaConnectorErrorHandler from '@/middleware/koa-connector-error-handle'; import koaConnectorErrorHandler from '@/middleware/koa-connector-error-handle';
import koaErrorHandler from '@/middleware/koa-error-handler'; import koaErrorHandler from '@/middleware/koa-error-handler';
import koaI18next from '@/middleware/koa-i18next'; import koaI18next from '@/middleware/koa-i18next';
import koaLog from '@/middleware/koa-log';
import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler'; import koaOIDCErrorHandler from '@/middleware/koa-oidc-error-handler';
import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler'; import koaSlonikErrorHandler from '@/middleware/koa-slonik-error-handler';
import koaSpaProxy from '@/middleware/koa-spa-proxy'; import koaSpaProxy from '@/middleware/koa-spa-proxy';
@ -22,8 +23,8 @@ export default async function initApp(app: Koa): Promise<void> {
app.use(koaSlonikErrorHandler()); app.use(koaSlonikErrorHandler());
app.use(koaConnectorErrorHandler()); app.use(koaConnectorErrorHandler());
// TODO move to specific router (LOG-454)
app.use(koaUserLog()); app.use(koaUserLog());
app.use(koaLog());
app.use(koaLogger()); app.use(koaLogger());
app.use(koaI18next()); app.use(koaI18next());

View file

@ -0,0 +1,110 @@
import { LogType, LogResult } from '@logto/schemas';
import { insertLog } from '@/queries/log';
import { createContextWithRouteParameters } from '@/utils/test-utils';
import koaLog, { WithLogContext, LogContext } from './koa-log';
const nanoIdMock = 'mockId';
jest.mock('@/queries/log', () => ({
insertLog: jest.fn(async () => Promise.resolve()),
}));
jest.mock('nanoid', () => ({
nanoid: jest.fn(() => nanoIdMock),
}));
describe('koaLog middleware', () => {
const insertLogMock = insertLog as jest.Mock;
const next = jest.fn();
const logMock: Partial<LogContext> = {
type: LogType.SignInUsernamePassword,
applicationId: 'foo',
userId: 'foo',
username: 'Foo Bar',
};
afterEach(() => {
jest.clearAllMocks();
});
it('insert log with success response', async () => {
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
log: {}, // Bypass middleware context type assert
};
next.mockImplementationOnce(async () => {
ctx.log = logMock;
});
await koaLog()(ctx, next);
const { type, ...rest } = logMock;
expect(insertLogMock).toBeCalledWith({
id: nanoIdMock,
type,
payload: {
...rest,
result: LogResult.Success,
},
});
});
it('should not block request if insertLog throws error', async () => {
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
log: {}, // Bypass middleware context type assert
};
const error = new Error('Failed to insert log');
insertLogMock.mockImplementationOnce(async () => {
throw error;
});
next.mockImplementationOnce(async () => {
ctx.log = logMock;
});
await koaLog()(ctx, next);
const { type, ...rest } = logMock;
expect(insertLogMock).toBeCalledWith({
id: nanoIdMock,
type,
payload: {
...rest,
result: LogResult.Success,
},
});
});
it('should insert log with failed result if next throws error', async () => {
const ctx: WithLogContext<ReturnType<typeof createContextWithRouteParameters>> = {
...createContextWithRouteParameters(),
log: {}, // Bypass middleware context type assert
};
const error = new Error('next error');
next.mockImplementationOnce(async () => {
ctx.log = logMock;
throw error;
});
await expect(koaLog()(ctx, next)).rejects.toMatchError(error);
const { type, ...rest } = logMock;
expect(insertLogMock).toBeCalledWith({
id: nanoIdMock,
type,
payload: {
...rest,
result: LogResult.Error,
error: String(error),
},
});
});
});

View file

@ -0,0 +1,55 @@
import { LogResult, LogType } from '@logto/schemas';
import { Context, MiddlewareType } from 'koa';
import { nanoid } from 'nanoid';
import { insertLog } from '@/queries/log';
export type WithLogContext<ContextT> = ContextT & {
log: LogContext;
};
export interface LogContext {
[key: string]: unknown;
type?: LogType;
}
const log = async (ctx: WithLogContext<Context>, result: LogResult) => {
const { type, ...rest } = ctx.log;
if (!type) {
return;
}
try {
await insertLog({
id: nanoid(),
type,
payload: {
...rest,
result,
},
});
} catch (error: unknown) {
console.error('An error occurred while inserting log');
console.error(error);
}
};
export default function koaLog<StateT, ContextT, ResponseBodyT>(): MiddlewareType<
StateT,
WithLogContext<ContextT>,
ResponseBodyT
> {
return async (ctx, next) => {
ctx.log = {};
try {
await next();
await log(ctx, LogResult.Success);
} catch (error: unknown) {
ctx.log.error = String(error);
await log(ctx, LogResult.Error);
throw error;
}
};
}

View file

@ -1,3 +1,4 @@
export * from './user';
export * from './connector'; export * from './connector';
export * from './log';
export * from './oidc-config'; export * from './oidc-config';
export * from './user';

View file

@ -0,0 +1,8 @@
export enum LogType {
SignInUsernamePassword = 'SignInUsernamePassword',
}
export enum LogResult {
Success = 'Success',
Error = 'Error',
}