From 1acce61851ec778254492a8ea29180e2fc221026 Mon Sep 17 00:00:00 2001 From: dillonstreator Date: Fri, 24 Jul 2026 10:38:42 -0500 Subject: [PATCH 1/2] Align request logging fields with OpenTelemetry semantic conventions. EOF Co-authored-by: Cursor --- .changeset/otel-request-log-fields.md | 5 +++++ src/app.ts | 26 ++++++++++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 .changeset/otel-request-log-fields.md diff --git a/.changeset/otel-request-log-fields.md b/.changeset/otel-request-log-fields.md new file mode 100644 index 0000000..3d6a8c2 --- /dev/null +++ b/.changeset/otel-request-log-fields.md @@ -0,0 +1,5 @@ +--- +"template-node-express": minor +--- + +Align request/response logging field names with OpenTelemetry semantic conventions. diff --git a/src/app.ts b/src/app.ts index 77a6d45..2d52687 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,6 +12,13 @@ import helmet from 'helmet'; import compression from 'compression'; import { getClientIp } from 'request-ip'; import * as ev from 'express-validator'; +import { + ATTR_CLIENT_ADDRESS, + ATTR_HTTP_REQUEST_METHOD, + ATTR_HTTP_RESPONSE_STATUS_CODE, + ATTR_URL_PATH, + ATTR_USER_AGENT_ORIGINAL, +} from '@opentelemetry/semantic-conventions'; import { Config } from './config'; export type App = { @@ -29,6 +36,9 @@ declare global { const LARGE_JSON_PATH = '/large-json-payload'; const APPLICATION_JSON = 'application/json'; +const HTTP_REQUEST_ID = 'http.request.id'; +const HTTP_REQUEST_BODY_SIZE = 'http.request.body.size'; +const HTTP_RESPONSE_BODY_SIZE = 'http.response.body.size'; export const initApp = async ( config: Config, @@ -61,7 +71,7 @@ export const initApp = async ( const requestId = req.headers['x-request-id']?.[0] || randomUUID(); - const l = logger.child({ requestId }); + const l = logger.child({ [HTTP_REQUEST_ID]: requestId }); let bytesRead = 0; req.on('data', (chunk: Buffer) => { @@ -89,13 +99,13 @@ export const initApp = async ( l.info( { duration: new Date().getTime() - start, - method: req.method, - path: req.path, - status: res.statusCode, - ua: req.headers['user-agent'], - ip: getClientIp(req), - br: bytesRead, - bw: bytesWritten, + [ATTR_HTTP_REQUEST_METHOD]: req.method, + [ATTR_URL_PATH]: req.path, + [ATTR_HTTP_RESPONSE_STATUS_CODE]: res.statusCode, + [ATTR_USER_AGENT_ORIGINAL]: req.headers['user-agent'], + [ATTR_CLIENT_ADDRESS]: getClientIp(req), + [HTTP_REQUEST_BODY_SIZE]: bytesRead, + [HTTP_RESPONSE_BODY_SIZE]: bytesWritten, }, 'Request handled' ); From 0038d5b16fd24d4820d37bcce32b90a2400498ba Mon Sep 17 00:00:00 2001 From: dillonstreator Date: Fri, 24 Jul 2026 12:19:28 -0500 Subject: [PATCH 2/2] Use nested req/res fields for request logging. Prefer pino-http-style names over OpenTelemetry attribute keys so application logs stay readable while OTel continues to own trace attributes. Co-authored-by: Cursor --- .changeset/otel-request-log-fields.md | 5 ---- .changeset/request-log-field-names.md | 5 ++++ src/app.ts | 33 ++++++++++++--------------- 3 files changed, 19 insertions(+), 24 deletions(-) delete mode 100644 .changeset/otel-request-log-fields.md create mode 100644 .changeset/request-log-field-names.md diff --git a/.changeset/otel-request-log-fields.md b/.changeset/otel-request-log-fields.md deleted file mode 100644 index 3d6a8c2..0000000 --- a/.changeset/otel-request-log-fields.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"template-node-express": minor ---- - -Align request/response logging field names with OpenTelemetry semantic conventions. diff --git a/.changeset/request-log-field-names.md b/.changeset/request-log-field-names.md new file mode 100644 index 0000000..423c953 --- /dev/null +++ b/.changeset/request-log-field-names.md @@ -0,0 +1,5 @@ +--- +"template-node-express": minor +--- + +Use clearer nested req/res field names in request logging (aligned with pino-http style). diff --git a/src/app.ts b/src/app.ts index 2d52687..1ab006c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,13 +12,6 @@ import helmet from 'helmet'; import compression from 'compression'; import { getClientIp } from 'request-ip'; import * as ev from 'express-validator'; -import { - ATTR_CLIENT_ADDRESS, - ATTR_HTTP_REQUEST_METHOD, - ATTR_HTTP_RESPONSE_STATUS_CODE, - ATTR_URL_PATH, - ATTR_USER_AGENT_ORIGINAL, -} from '@opentelemetry/semantic-conventions'; import { Config } from './config'; export type App = { @@ -36,9 +29,6 @@ declare global { const LARGE_JSON_PATH = '/large-json-payload'; const APPLICATION_JSON = 'application/json'; -const HTTP_REQUEST_ID = 'http.request.id'; -const HTTP_REQUEST_BODY_SIZE = 'http.request.body.size'; -const HTTP_RESPONSE_BODY_SIZE = 'http.response.body.size'; export const initApp = async ( config: Config, @@ -71,7 +61,7 @@ export const initApp = async ( const requestId = req.headers['x-request-id']?.[0] || randomUUID(); - const l = logger.child({ [HTTP_REQUEST_ID]: requestId }); + const l = logger.child({ req: { id: requestId } }); let bytesRead = 0; req.on('data', (chunk: Buffer) => { @@ -98,14 +88,19 @@ export const initApp = async ( res.on('finish', () => { l.info( { - duration: new Date().getTime() - start, - [ATTR_HTTP_REQUEST_METHOD]: req.method, - [ATTR_URL_PATH]: req.path, - [ATTR_HTTP_RESPONSE_STATUS_CODE]: res.statusCode, - [ATTR_USER_AGENT_ORIGINAL]: req.headers['user-agent'], - [ATTR_CLIENT_ADDRESS]: getClientIp(req), - [HTTP_REQUEST_BODY_SIZE]: bytesRead, - [HTTP_RESPONSE_BODY_SIZE]: bytesWritten, + req: { + id: requestId, + method: req.method, + path: req.path, + userAgent: req.headers['user-agent'], + ip: getClientIp(req), + bytesRead, + }, + res: { + statusCode: res.statusCode, + bytesWritten, + }, + responseTime: new Date().getTime() - start, }, 'Request handled' );