Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d47038f
feat(middleware): actionLogger middleware
JamesTLopez May 31, 2026
a1b2f81
fix: pnpm-workspace push
JamesTLopez May 31, 2026
f22c5c0
feat(routes): add action logger middleware
JamesTLopez May 31, 2026
3f14bef
chore: code cleanup
JamesTLopez May 31, 2026
6bcfce8
fix: eslint fix
JamesTLopez May 31, 2026
383cb4b
chore: simplify comment
JamesTLopez Jun 1, 2026
026b3ed
chore: remove useless utils
JamesTLopez Jun 1, 2026
4bdbece
fix: categoryRouter using hardcoded boolean
JamesTLopez Jun 2, 2026
3d9ffbc
Merge branch 'main' into feat/174-lyric-logging
JamesTLopez Jun 10, 2026
e8f57ab
feat: re-add params/body extraction
JamesTLopez Jun 10, 2026
7e45905
feat: add loggerEnabled config
JamesTLopez Jun 11, 2026
6f9e7a7
feat: update loggerEnabled in worker
JamesTLopez Jun 11, 2026
40c2a68
feat: move logger up
JamesTLopez Jun 11, 2026
4c1fa26
fix: add user to logger and remove duration errormessage
JamesTLopez Jun 14, 2026
b2d4d0d
fix: pass user to formatActionLog
JamesTLopez Jun 14, 2026
0fd47bc
feat: attach requestContext to errorHandler
JamesTLopez Jun 14, 2026
b09bf7a
fix: remove logging from controllers
JamesTLopez Jun 14, 2026
af55190
fix: rename disableLogger
JamesTLopez Jun 14, 2026
5dc7893
fix: remove debug comment
JamesTLopez Jun 14, 2026
d2e21cf
feat: update comment
JamesTLopez Jun 14, 2026
d3aa3ca
Merge branch 'main' into feat/174-lyric-logging
JamesTLopez Jun 30, 2026
37b81d8
Merge branch 'main' into feat/174-lyric-logging
JamesTLopez Jul 20, 2026
ca86a3d
fix: change send to json
JamesTLopez Jul 20, 2026
3067cb9
fix: rename logger config
JamesTLopez Jul 20, 2026
bca1612
fix: remove should log route
JamesTLopez Jul 20, 2026
cd6c961
fix: update naming
JamesTLopez Jul 20, 2026
9890e16
fix: flip enable condition
JamesTLopez Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/data-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export {
export { errorHandler } from './src/middleware/errorHandler.js';
export { type DbConfig, migrate } from '@overture-stack/lyric-data-model';

// middleware
export { actionLoggerMiddleware } from './src/middleware/actionLogger.js';

// routes
export { default as dictionaryRouters } from './src/routers/dictionaryRouter.js';
export { default as submissionRouter } from './src/routers/submissionRouter.js';
Expand Down
48 changes: 48 additions & 0 deletions packages/data-provider/src/middleware/actionLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { NextFunction, Response } from 'express';

import type { Logger } from '../config/logger.js';
import { ActionResult, extractActionMetadata, formatActionLog } from '../utils/actionLoggerUtils.js';
import type { RequestWithUser } from './auth.js';

export type ActionLoggerConfig = {
enabled: boolean;
};

/**
* Action Logger Middleware
* This middleware should be placed after authMiddleware in the router chain.
*/
export const actionLoggerMiddleware = (config: ActionLoggerConfig, logger: Logger) => {
return (req: RequestWithUser, res: Response, next: NextFunction) => {
// Skip logging if disabled
if (!config.enabled) {
return next();
}
// Extract request metadata
const metadata = extractActionMetadata(req);
const startTime = Date.now();

const logAction = (statusCode: number) => {
const duration = Date.now() - startTime;
const statusResult = statusCode >= 200 && statusCode < 400 ? ActionResult.ALLOWED : ActionResult.DENIED;

const logMessage = formatActionLog(metadata, statusResult, statusCode, duration);

// Use appropriate log level based on statusCode returned
if (statusResult === ActionResult.DENIED) {
logger.warn(logMessage);
return;
}
logger.info(logMessage);
};

/**
* Log the action after response is sent
*/
res.on('finish', () => {
logAction(res.statusCode);
});

next();
};
};
2 changes: 2 additions & 0 deletions packages/data-provider/src/routers/auditRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express';

import { BaseDependencies } from '../config/config.js';
import auditController from '../controllers/auditController.js';
import { actionLoggerMiddleware } from '../middleware/actionLogger.js';
import { type AuthConfig, authMiddleware } from '../middleware/auth.js';

const router = ({
Expand All @@ -16,6 +17,7 @@ const router = ({
router.use(json());

router.use(authMiddleware(authConfig));
router.use(actionLoggerMiddleware({ enabled: authConfig.enabled }, baseDependencies.logger));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having actionLoggerMiddleware after the authMiddleware won't be called on an unauthorized request.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of clinical there's already an implementation of a request logger, which I think will duplicate some information..


router.get(
'/category/:categoryId/organization/:organization',
Expand Down
2 changes: 2 additions & 0 deletions packages/data-provider/src/routers/categoryRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express';

import { BaseDependencies } from '../config/config.js';
import categoryController from '../controllers/categoryController.js';
import { actionLoggerMiddleware } from '../middleware/actionLogger.js';
import { type AuthConfig, authMiddleware } from '../middleware/auth.js';

const router = ({
Expand All @@ -16,6 +17,7 @@ const router = ({
router.use(json());

router.use(authMiddleware(authConfig));
router.use(actionLoggerMiddleware({ enabled: authConfig.enabled }, baseDependencies.logger));

router.get('/', categoryController(baseDependencies).listAll);
router.get('/:categoryId', categoryController(baseDependencies).getDetails);
Expand Down
2 changes: 2 additions & 0 deletions packages/data-provider/src/routers/dictionaryRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express';

import { BaseDependencies } from '../config/config.js';
import dictionaryController from '../controllers/dictionaryController.js';
import { actionLoggerMiddleware } from '../middleware/actionLogger.js';
import { type AuthConfig, authMiddleware } from '../middleware/auth.js';

const router = ({
Expand All @@ -16,6 +17,7 @@ const router = ({
router.use(json());

router.use(authMiddleware(authConfig));
router.use(actionLoggerMiddleware({ enabled: authConfig.enabled }, baseDependencies.logger));

router.post('/register', dictionaryController(baseDependencies).registerDictionary);

Expand Down
2 changes: 2 additions & 0 deletions packages/data-provider/src/routers/submissionRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import multer from 'multer';

import { BaseDependencies } from '../config/config.js';
import createSubmissionController from '../controllers/submissionController.js';
import { actionLoggerMiddleware } from '../middleware/actionLogger.js';
import { type AuthConfig, authMiddleware } from '../middleware/auth.js';

const router = ({
Expand Down Expand Up @@ -68,6 +69,7 @@ const router = ({
);

router.use(authMiddleware(authConfig));
router.use(actionLoggerMiddleware({ enabled: authConfig.enabled }, baseDependencies.logger));

router.get('/:submissionId', submissionController.getSubmissionById);

Expand Down
2 changes: 2 additions & 0 deletions packages/data-provider/src/routers/submittedDataRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express';

import { BaseDependencies } from '../config/config.js';
import submittedDataController from '../controllers/submittedDataController.js';
import { actionLoggerMiddleware } from '../middleware/actionLogger.js';
import { type AuthConfig, authMiddleware } from '../middleware/auth.js';

const router = ({
Expand All @@ -16,6 +17,7 @@ const router = ({
router.use(json());

router.use(authMiddleware(authConfig));
router.use(actionLoggerMiddleware({ enabled: authConfig.enabled }, baseDependencies.logger));

router.get(
'/category/:categoryId',
Expand Down
2 changes: 2 additions & 0 deletions packages/data-provider/src/routers/validationRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { json, Router, urlencoded } from 'express';

import { BaseDependencies, type ValidatorConfig } from '../config/config.js';
import validationController from '../controllers/validationController.js';
import { actionLoggerMiddleware } from '../middleware/actionLogger.js';
import { type AuthConfig, authMiddleware } from '../middleware/auth.js';

const router = ({
Expand All @@ -18,6 +19,7 @@ const router = ({
router.use(json());

router.use(authMiddleware(authConfig));
router.use(actionLoggerMiddleware({ enabled: authConfig.enabled }, baseDependencies.logger));

router.get(
'/category/:categoryId/entity/:entityName/exists',
Expand Down
69 changes: 69 additions & 0 deletions packages/data-provider/src/utils/actionLoggerUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { RequestWithUser } from '../middleware/auth.js';

export const ActionType = {
READ: 'READ',
WRITE: 'WRITE',
} as const;

export type ActionTypeValues = (typeof ActionType)[keyof typeof ActionType];

export const ActionResult = {
ALLOWED: 'ALLOWED',
DENIED: 'DENIED',
} as const;

export type ActionResultValues = (typeof ActionResult)[keyof typeof ActionResult];

export interface ActionLogMetadata {
action: ActionTypeValues;
method: string;
path: string;
categoryId?: number;
organization?: string;
userId?: string;
entityName?: string;
systemId?: string;
submissionId?: number;
Comment thread
JamesTLopez marked this conversation as resolved.
}

/**
* Determines if an HTTP method represents a read or write operation
*/
export const getActionType = (method: string): ActionTypeValues => {
return method === 'GET' || method === 'HEAD' ? ActionType.READ : ActionType.WRITE;
};

/**
* Extracts all relevant metadata from a request for action logging
*/
export const extractActionMetadata = (req: RequestWithUser): ActionLogMetadata => {
return {
action: getActionType(req.method),
method: req.method,
path: req.originalUrl || req.path,
};
};

/**
* Formats action log metadata into a readable string for logging
*/
export const formatActionLog = (
metadata: ActionLogMetadata,
statusResult: ActionResultValues,
statusCode: number,
duration: number,
errorMessage?: string,
): string => {
const actionLogResult = [`ACTION_LOG - PATH=${metadata.path}`, `type=|${metadata.action}-${metadata.method}|`];

actionLogResult.push(`userId: ${metadata.userId || 'null'}`);
actionLogResult.push(`result: ${statusResult}`);
actionLogResult.push(`status: ${statusCode}`);
actionLogResult.push(`duration: ${duration}ms`);

if (errorMessage) {
actionLogResult.push(`error: ${errorMessage}`);
}

return actionLogResult.join(' | ');
};
7 changes: 7 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ packages:
- 'apps/*'
# all packages in subdirs of packages/
- 'packages/*'
allowBuilds:
'@scarf/scarf': false
cpu-features: false
es5-ext: false
esbuild: false
protobufjs: false
ssh2: false