How to optionally set Winston transports when logging to Stackdriver from GKE

10/29/2019

I have a Node.js app which runs in a Docker container in Google Kubernetes Engine. I have set up a logging class which uses Winston (v3.2.1) with two transports defined; one to log to the console and one to log to Stackdriver (using @google-cloud/logging-winston (v3.0.0)).

With both transports defined, all is good and I can see the logs in Stackdriver. The console logs go to projects/[project-id]/logs/stdout and the Stackdriver logs go to projects/[project-id]/logs/winston_log.

However, I want to configure the logger so that when debugging locally, logs are only sent to the console and when running in GKE, logs are only sent to Stackdriver, as follows:

  // Configure console logger
  private readonly consoleLogger = new winston.transports.Console({
    format: combine(
      colorize(),
      simple(),
      printf(context => {
        return `[${context.level}]${context.message}`;
      }),
    ),
  });

  // Configure Stackdriver logger
  private readonly stackdriverLogger = new LoggingWinston({
    serviceContext: {
      service: this.serviceName,
    },
  });

  // Create Winston logger
  private readonly logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: json(),
    defaultMeta: {
      service: this.serviceName,
    },
    // This line does not work:
    transports: [process.env.NODE_ENV === 'development' ? this.consoleLogger : this.stackdriverLogger],
  });

The aim here is that if the NODE_ENV is development, use the console logger, otherwise use the Stackdriver logger. However, when I deploy this to GKE, I see the following errors in the Stackdriver console log (and nothing in projects/[project-id]/logs/winston_log):

[winston] Attempt to write logs with no transports { // Logged message }

When I run this code locally on my dev machine with NODE_ENV=development, I see the logs in my local console and if I set NODE_ENV=production I see the logs in Stackdriver.

If I remove the ternary operator and have both transports defined and deploy to GKE, I do not see the above error and logging works correctly to both transports:

transports: [this.consoleLogger, this.stackdriverLogger],

Can anyone help me to configure this correctly?

EDIT

Added the full Logger.ts file for context:

import { LoggerService } from '@nestjs/common';
import * as winston from 'winston';
const { colorize, combine, json, printf, simple } = winston.format;
import { LoggingWinston } from '@google-cloud/logging-winston';
import cls from 'cls-hooked';
import { ConfigManager } from '../config';
import { TraceId } from '../middleware/traceId/constants';

export class Logger implements LoggerService {
  private readonly serviceName: string = process.env.SERVICE_NAME;

  private readonly consoleLogger = new winston.transports.Console({
    format: combine(
      colorise(),
      simple(),
      printf(context => {
        return `[${context.level}]${context.message}`;
      }),
    ),
  });

  private stackdriverLogger = new LoggingWinston({
    serviceContext: {
      service: this.serviceName,
    },
  });

  private readonly logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: json(),
    defaultMeta: {
      service: this.serviceName,
    },
    transports: [process.env.NODE_ENV === 'development' ? this.consoleLogger : this.stackdriverLogger]
  });

  constructor(private readonly context?: string) {}

  public verbose(message: string, context?: string) {
    const log = this.buildLog(message, context);
    this.logger.verbose(log.message, log.metadata);
  }

  public debug(message: string, context?: string) {
    const log = this.buildLog(message, context);
    this.logger.debug(log.message, log.metadata);
  }

  public log(message: string, context?: string) {
    const log = this.buildLog(message, context);
    this.logger.info(log.message, log.metadata);
  }

  public warn(message: string, context?: string) {
    const log = this.buildLog(message, context);
    this.logger.warn(log.message, log.metadata);
  }

  public error(message: string, trace?: string, context?: string) {
    const log = this.buildLog(message, context, trace);
    this.logger.error(log.message, log.metadata);
  }

  private buildLog(message: string, context?: string, trace?: string) {
    const ctx = context || this.context;
    const traceId = this.getTraceId();

    return {
      message: `[${ctx}] ${message}`,
      metadata: {
        traceId,
        source: ctx,
        stackTrace: trace,
      },
    };
  }

  private getTraceId(): string {
    const clsNamespace = cls.getNamespace(TraceId.Namespace);
    if (!clsNamespace) {
      return null;
    }
    return clsNamespace.get(TraceId.Key);
  }
}
-- Andrew Ridout
google-cloud-platform
google-cloud-stackdriver
google-kubernetes-engine
node.js
winston

1 Answer

3/13/2020

So it turns out the problem was the @google-cloud/logging-winston package had a bug in it which caused it to throw this error:

UnhandledPromiseRejectionWarning: FetchError: request to http://169.254.169.254/computeMetadata/v1/instance failed, reason: connect ECONNREFUSED 169.254.169.254:80

This has now been fixed in version 3.0.6 - see https://github.com/googleapis/nodejs-logging-winston/issues/389#issuecomment-593727968.

After updating @google-cloud/logging-winston, the Winston logging is working correctly for me in Stackdriver.

-- Andrew Ridout
Source: StackOverflow