Well-formatted logs for Google Cloud Logging
2025-03-20
Node.jsGCPIn writing logs in applications that run on Google Cloud Platform, it requires some configurations to be compiled to trace logs in Cloud Logging. This post describes how to implement it, and also some tips for making logs more useful and informative.
Tracked as trace logs in Cloud Logging
Node.js applications can out logs with just using console.log
or console.error
, and they will be collected and can be seen in Cloud Logging. However, they are not compiled to trace logs. It means that it's hard to know which request the log belongs to.
The following code is an example of writing logs to get compiled to trace logs.
const writeLog = (req: Request, message: string): void => {
const project = process.env.GOOGLE_CLOUD_PROJECT
const traceHeader = req.headers.get('X-Cloud-Trace-Context')
const [trace] = traceHeader.split('/')
const base = {
url: req.url,
'logging.googleapis.com/trace': `projects/${project}/traces/${trace}`,
}
console.log({
message,
...base,
severity: 'INFO',
})
}
Google Cloud tweaks requests to inject X-Cloud-Trace-Context
header to requests for tracing.
Inserting the X-Cloud-Trace-Context
header value in logging.googleapis.com/trace
field makes the log be compiled to trace logs.
See Logging agent: special JSON fields for more details.
Including Stack Trace
In error logs, it's very useful to include stack trace for diagnosing.
From here, I'll show how to implement it using winston
.
winston
provides errors({ stack: true })
to include stack trace as the document shows, but it only works when given object is an instance of Error
.
const error = new Error('test')
logger.error(error) // includes stack trace, but no additional message
logger.error({ message: 'failed', error }) // has additional message, but no stack trace
To solve this, it needs to implement a custom format like the following.
import winston from 'winston'
const includeStackTrace = winston.format(
(log): winston.Logform.TransformableInfo => {
const severity = log.level.toUpperCase()
const error = (() => {
if ('error' in log) {
if ((log as any).error instanceof Error) {
return log.error.toString()
}
} else if (typeof log.message === 'object' && 'error' in log.message) {
if ((log.message as any).error instanceof Error) {
return (log.message as any).error.toString()
}
}
return log.error
})()
const stack = (() => {
if (
'error' in log &&
(log as any).error instanceof Error &&
(log as any).error.stack &&
!('stack' in log)
) {
return (log as any).error.stack
} else if (
typeof log.message === 'object' &&
'error' in log.message &&
'stack' in (log.message as any).error &&
!('stack' in log.message)
) {
return (log.message as any).error.stack
}
})()
const result = Object.assign({}, log, {
severity,
stack,
error,
})
return result
},
)
This allows logs to include the stack trace of an Error object that is passed in the error
field of the given object.
You can use this format as below:
const {
combine,
json,
timestamp,
errors,
uncolorize,
prettyPrint,
} = winston.format
const format = combine(
errors({ stack: true }),
timestamp({ format: 'YYYY-MM-DD_HH:mm:ss' }),
includeStackTrace(),
prettyPrint(),
json(),
uncolorize(),
)
Create Logger
Creating a logger looks like the following code snippet:
import winston from 'winston'
const logger = winston.createLogger({
level: 'info',
format,
transports: [new winston.transports.Console()],
})
What logs are written looks like is: