From 5b8ba7d1837b13b3328b3cca769cb8d8edb9fc4b Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 9 Jul 2026 10:46:09 +0100 Subject: [PATCH 1/4] Revert "Merge pull request #16341 from guardian/aa/container-logs" This reverts commit bfac6ab14d6166945e84133f225a83d2138e6c73, reversing changes made to d55fe3173814be56cf17be6b56ce17696cc69408. --- Production.dockerfile | 3 +++ dotcom-rendering/src/server/lib/logging.ts | 8 -------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Production.dockerfile b/Production.dockerfile index 405bd27dc9b..c31c688555c 100644 --- a/Production.dockerfile +++ b/Production.dockerfile @@ -23,6 +23,9 @@ FROM dhi.io/node:24-alpine3.23 AS application WORKDIR /app COPY --from=builder --chown=node:node /app/dotcom-rendering/dist /app +# Disable logging with Log4js as console logs will be forwarded to Central ELK with a sidecar +# TODO Maintain metrics +ENV DISABLE_LOGGING_AND_METRICS=true ENV NODE_ENV=production # Expose the port that the application listens on diff --git a/dotcom-rendering/src/server/lib/logging.ts b/dotcom-rendering/src/server/lib/logging.ts index e595e08841b..0e2c2b0794e 100644 --- a/dotcom-rendering/src/server/lib/logging.ts +++ b/dotcom-rendering/src/server/lib/logging.ts @@ -110,7 +110,6 @@ const enableLog4js: Configuration = { production: { appenders: ['out', 'fileAppender'], level: 'info' }, code: { appenders: ['out', 'fileAppender'], level: 'debug' }, development: { appenders: ['console'], level: 'debug' }, - container: { appenders: ['out'], level: 'info' }, }, // log4js cluster mode handling does not work as it prevents // logs from processes other than the main process from @@ -139,13 +138,6 @@ const getLoggerCategory = (): string => { if (process.env.DISABLE_LOGGING_AND_METRICS === 'true') { return 'off'; } - - // Are we running in a container? - // See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-environment-variables.html - if (process.env.AWS_EXECUTION_ENV?.startsWith('AWS_ECS_') === true) { - return 'container'; - } - if (process.env.NODE_ENV === 'development') { return 'development'; } From 815e27089bcecf980822a9d98277034636d6b7f8 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 9 Jul 2026 10:50:24 +0100 Subject: [PATCH 2/4] refactor: Encapsulate EC2 Log4JS configuration in a function --- dotcom-rendering/src/server/lib/logging.ts | 80 +++++++++++----------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/dotcom-rendering/src/server/lib/logging.ts b/dotcom-rendering/src/server/lib/logging.ts index 0e2c2b0794e..b2e9d8ee94e 100644 --- a/dotcom-rendering/src/server/lib/logging.ts +++ b/dotcom-rendering/src/server/lib/logging.ts @@ -4,14 +4,6 @@ import type { Configuration, Layout, LoggingEvent } from 'log4js'; import { addLayout, configure, getLogger, shutdown } from 'log4js'; import { type DCRLoggingStore, loggingStore } from './logging-store'; -const logName = `dotcom-rendering.log`; - -const logLocation = - process.env.NODE_ENV === 'production' && - !process.env.DISABLE_LOGGING_AND_METRICS - ? `/var/log/dotcom-rendering/${logName}` - : `${path.resolve('logs')}/${logName}`; - type LogFields = Partial & Record; @@ -84,38 +76,48 @@ const disableLog4js: Configuration = { }, }; -const enableLog4js: Configuration = { - appenders: { - console: { - type: 'console', - layout: consoleLayout, +function configureLog4jsEC2() { + const logName = `dotcom-rendering.log`; + + const logLocation = + process.env.NODE_ENV === 'production' && + !process.env.DISABLE_LOGGING_AND_METRICS + ? `/var/log/dotcom-rendering/${logName}` + : `${path.resolve('logs')}/${logName}`; + + configure({ + appenders: { + console: { + type: 'console', + layout: consoleLayout, + }, + fileAppender: { + type: 'file', + filename: logLocation, + maxLogSize: '5M', + backups: 5, + compress: true, + layout: { type: 'json', separator: ',' }, + // Owner Read & Write, Group Read + mode: 0o640, + }, + out: { + type: 'stdout', + layout: { type: 'json', separator: ',' }, + }, }, - fileAppender: { - type: 'file', - filename: logLocation, - maxLogSize: '5M', - backups: 5, - compress: true, - layout: { type: 'json', separator: ',' }, - // Owner Read & Write, Group Read - mode: 0o640, + categories: { + default: { appenders: ['out'], level: 'off' }, + production: { appenders: ['out', 'fileAppender'], level: 'info' }, + code: { appenders: ['out', 'fileAppender'], level: 'debug' }, + development: { appenders: ['console'], level: 'debug' }, }, - out: { - type: 'stdout', - layout: { type: 'json', separator: ',' }, - }, - }, - categories: { - default: { appenders: ['out'], level: 'off' }, - production: { appenders: ['out', 'fileAppender'], level: 'info' }, - code: { appenders: ['out', 'fileAppender'], level: 'debug' }, - development: { appenders: ['console'], level: 'debug' }, - }, - // log4js cluster mode handling does not work as it prevents - // logs from processes other than the main process from - // writing to the log. - disableClustering: true, -}; + // log4js cluster mode handling does not work as it prevents + // logs from processes other than the main process from + // writing to the log. + disableClustering: true, + }); +} // We do this to ensure no memory leaks during development as hot reloading // doesn't clear up old listeners. @@ -131,7 +133,7 @@ if (process.env.NODE_ENV === 'development') { if (process.env.DISABLE_LOGGING_AND_METRICS === 'true') { configure(disableLog4js); } else { - configure(enableLog4js); + configureLog4jsEC2(); } const getLoggerCategory = (): string => { From b852da56f6b514bf68ecbcbdd0506a80bb0a1b5a Mon Sep 17 00:00:00 2001 From: akash1810 Date: Thu, 9 Jul 2026 10:55:40 +0100 Subject: [PATCH 3/4] feat: Configure Log4JS when running in ECS Within ECS, it is common to write logs to stdout/stderr and have a sidecar process the logs. This change configures Log4JS for this. Note that there is only one appender, as it looks like the presence of a `fileAppender` causes Log4JS to attempt a `mkdir` which is currently restricted in our image. See also https://github.com/guardian/dotcom-rendering/pull/16341. --- dotcom-rendering/src/server/lib/logging.ts | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/server/lib/logging.ts b/dotcom-rendering/src/server/lib/logging.ts index b2e9d8ee94e..899cfb07689 100644 --- a/dotcom-rendering/src/server/lib/logging.ts +++ b/dotcom-rendering/src/server/lib/logging.ts @@ -119,6 +119,27 @@ function configureLog4jsEC2() { }); } +function configureLog4jsECS() { + configure({ + appenders: { + out: { + type: 'stdout', + layout: { type: 'json', separator: ',' }, + }, + }, + categories: { + default: { appenders: ['out'], level: 'info' }, + production: { appenders: ['out'], level: 'info' }, + code: { appenders: ['out'], level: 'debug' }, + development: { appenders: ['out'], level: 'debug' }, + }, + // log4js cluster mode handling does not work as it prevents + // logs from processes other than the main process from + // writing to the log. + disableClustering: true, + }); +} + // We do this to ensure no memory leaks during development as hot reloading // doesn't clear up old listeners. if (process.env.NODE_ENV === 'development') { @@ -133,7 +154,15 @@ if (process.env.NODE_ENV === 'development') { if (process.env.DISABLE_LOGGING_AND_METRICS === 'true') { configure(disableLog4js); } else { - configureLog4jsEC2(); + // See https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-environment-variables.html + const runningInECS = + process.env.AWS_EXECUTION_ENV?.startsWith('AWS_ECS_') === true; + + if (runningInECS) { + configureLog4jsECS(); + } else { + configureLog4jsEC2(); + } } const getLoggerCategory = (): string => { From e6baf156170a6d8a149f9c5105b54ca723b25152 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 7 Jul 2026 15:25:33 +0100 Subject: [PATCH 4/4] feat: Add `docker-compose.yml` to run the production image locally The aim here is to confirm the production image works before running on AWS ECS, as its slightly easier to debug any issues locally. --- Production.dockerfile | 3 --- docker-compose.yaml | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 docker-compose.yaml diff --git a/Production.dockerfile b/Production.dockerfile index c31c688555c..405bd27dc9b 100644 --- a/Production.dockerfile +++ b/Production.dockerfile @@ -23,9 +23,6 @@ FROM dhi.io/node:24-alpine3.23 AS application WORKDIR /app COPY --from=builder --chown=node:node /app/dotcom-rendering/dist /app -# Disable logging with Log4js as console logs will be forwarded to Central ELK with a sidecar -# TODO Maintain metrics -ENV DISABLE_LOGGING_AND_METRICS=true ENV NODE_ENV=production # Expose the port that the application listens on diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000000..3fd54642df3 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,53 @@ +# A Docker Compose file for running the production build locally. +# Usage: +# docker compose up -d --build --force-recreate +services: + tag-page-rendering: + build: + dockerfile: ./Production.dockerfile + environment: + NODE_ENV: production + GU_STAGE: PROD + GU_APP: tag-page-rendering + GU_STACK: frontend + + # We configure Log4JS based on this environment variable. See `server/lib/logging.ts`. + AWS_EXECUTION_ENV: AWS_ECS_LOCAL + + # Explicitly tell AWS SDK where to find credentials + AWS_SHARED_CREDENTIALS_FILE: /.aws/credentials + ports: + - '9000:9000' + + # Share the host's AWS credentials with the container + volumes: + - ${HOME}/.aws/credentials:/.aws/credentials:ro + + # In Production.dockerfile, we're deliberately using a minimal image that does not have `curl` installed. + # Therefore, we're using Node to make a request to the healthcheck endpoint instead of using `curl`. + healthcheck: + test: + [ + 'CMD', + 'node', + '-e', + "fetch('http://localhost:9000/_healthcheck').then(_ => process.exit(0)).catch(_ => process.exit(1))", + ] + interval: 10s + timeout: 5s + retries: 5 + + # This service is used to make a sample request to tag-page-rendering only after it has started and is healthy. + # It exits immediately after making the request, so it is not a long-running service. + # View the logs via: + # docker logs "$(docker ps -aq --filter "name=sample-request" --latest)" + # The log output is the DCR response, so we can also pipe it through `jq` to pretty-print it, e.g.: + # docker logs "$(docker ps -aq --filter "name=sample-request" --latest)" | jq . + sample-request: + image: curlimages/curl:8.21.0 + depends_on: + tag-page-rendering: + condition: service_healthy + command: | + curl "https://www.theguardian.com/tone/minutebyminute.json?dcr=true" --silent > data.json && \ + curl -X POST http://localhost:9000/TagPage -d @data.json -H "Content-Type: application/json"