Firebase functions: logging with winston in stackdriver console

Viewed 1549

I cannot make winston logger to write logs to stackdriver console. I deploy my functions as google firebase functions (using firebase deploy). console logging works fine, but we don't use such tool in the project.

What I tried:

Please suggest... I'm tired of experiments (each re-deploy takes time)

3 Answers

Winston's default Console transport fails because it uses console._stdout.write when it's available, which is not accepted by Firebase Functions.

There's now a Google Cloud transport package for Stackdriver you can try. Haven't used it and it requires node ^8.11.2 if you're using Winston 3.

Docs for node.js winston setup are here and here

I've added my full logger.js setup below.

The important bit is:

const format = winston.format.combine(winston.format.colorize({ all: true }))
const console = new winston.transports.Console({ format: winston.format.combine(format) })

const options = this.#explicitSetup ? { projectId: appConfig.firebase.options.projectId, keyFilename: `${rootDirname}/service-account-file.json` } : {}
const loggingWinston = new LoggingWinston(options)

const transports = emulators ? [console] : [console, loggingWinston]

this.#logger = winston.createLogger({
  level: this.#defaultLevel,
  transports
})

Basically, if the emulators are running then use the console logger else use the console logger and the stack driver transports. You can check if the emulators are running by pinging a functions endpoint (e.g. a /ping endpoint you have created) on localhost. If it does not exist then the emulators are not running or this is a production environment. Notice also the ability to use an explicit setup whereby the projectId and keyFilename are passed in.

The JSON file for keyFilename can be created here:

https://cloud.google.com/docs/authentication/getting-started

My full logger.js code, in case it helps, follows:

import winston from 'winston'
import { LoggingWinston } from '@google-cloud/logging-winston'
import { appConfig } from '../app-config.js'
import { rootDirname } from './root-dirname.js'
import { isObjectLike } from 'lodash-es'

// https://cloud.google.com/logging/docs/setup/nodejs

export class Logger {

  #logger
  #defaultLevel = 'debug'
  #explicitSetup = false

  constructor() {
    this.error = this.error.bind(this)
    this.warn = this.warn.bind(this)
    this.info = this.info.bind(this)
    this.debug = this.debug.bind(this)
    this.log = this.log.bind(this)
  }

  init(emulators) {

    // https://stackoverflow.com/a/64173978/1205871
    winston.addColors({
      error: 'red',
      warn: 'yellow',
      info: 'bold cyan',
      debug: 'bold green'
    })

    const format = winston.format.combine(winston.format.colorize({ all: true }))
    const console = new winston.transports.Console({ format: winston.format.combine(format) })

    const options = this.#explicitSetup ? { projectId: appConfig.firebase.options.projectId, keyFilename: `${rootDirname}/service-account-file.json` } : {}
    const loggingWinston = new LoggingWinston(options)

    const transports = emulators ? [console] : [console, loggingWinston]

    this.#logger = winston.createLogger({
      level: this.#defaultLevel,
      transports
    })
  }

  error(...args) {
    this.#logger.error(this.#argsToString(args))
  }

  warn(...args) {
    this.#logger.warn(this.#argsToString(args))
  }

  info(...args) {
    this.#logger.info(this.#argsToString(args))
  }

  debug(...args) {
    this.#logger.debug(this.#argsToString(args))
  }

  log(...args) {
    this.#logger[this.#defaultLevel](this.#argsToString(args))
  }

  #argsToString(args) {
    return args.map(arg => {
      const str = isObjectLike(arg) ? JSON.stringify(arg) : arg.toString()
      return str.trim()
    }).join(' \u2022\u2022 ')
  }
}

const blogger = new Logger()
export const logger = blogger
Related