Dynamic Log Control for Log Libraries

balance

This post is part 2 of the Dynamic Logging series and describes how to implement dynamic logging with several popular logging libraries.

Please first read part 1 at Dynamic Serverless Log Control which explains the technique of dynamic log control and how it works.

Controlling Various Loggers

This post describes the code required to implement dynamic log control for several popular and suitable logging libraries.

When selecting your logging library, here are several important factets it should exhibit:

Controlling SenseLogs

Here is a sample of how to use SenseLogs which is an extremely fast logger with built in support to respond to the LOG_FILTER, LOG_OVERRIDE and LOG_SAMPLE environment variables. No extra glue code is required.

You can also access these code snippets via GitHub gists at: Dynamic Log Control Gist

import SenseLogs from 'senselogs'

let log = new SenseLogs()

exports.handler = async (event, context) => {
    //  This will be emitted if LOG_FILTER contains 'debug' as a log level
    log.debug('Hello world')
    //  EMF metrics can also be dynamically controlled
    log.metrics('metrics', 'AcmeRockets/CriticalLaunchFailure', {explosion: 1})
}

The SenseLogs logging library is available from GitHub SenseLogs or NPM SenseLogs.

Controlling Debug

Here is a sample of how to use the Debug logger.

The Debug logger is a more primitive logger and is restricted to a single log level at a time.

import Debug from 'debug'

let {LOG_FILTER, LOG_OVERRIDE, LOG_SAMPLE} = process.env

if (LOG_OVERRIDE != null) {
    let [expire, level] = LOG_OVERRIDE.split(':')
    if (level && Date.now() < expire) {
        LOG_FILTER = level
    }
}
let sample = 0, sampleRate, sampleLevel
if (LOG_SAMPLE != null) {
    [sampleRate, sampleLevel] = LOG_SAMPLE.split(':')
}

Debug.enable(LOG_FILTER)

exports.handler = async (event, context) => {
    if (sampleRate && (sample++ % (100 / sampleRate)) == 0) {
        debug.enable(sampleLevel)
    }
    Debug('debug')('Debug message')
}

Controlling Bunyan

Here is a sample of how to use the Bunyan logger.

import Bunyan from 'bunyan'

let {LOG_FILTER, LOG_OVERRIDE, LOG_SAMPLE} = process.env

if (LOG_OVERRIDE != null) {
    let [expire, level] = LOG_OVERRIDE.split(':')
    if (level && Date.now() < expire) {
        LOG_FILTER = level
    }
}
let sample = 0, sampleRate, sampleLevel
if (LOG_SAMPLE != null) {
    [sampleRate, sampleLevel] = LOG_SAMPLE.split(':')
}

const bunyan = Bunyan.createLogger({name: 'bunyan', level: LOG_FILTER})

exports.handler = async (event, context) => {
    if (sampleRate && (sample++ % (100 / sampleRate)) == 0) {
        bunyan.level = sampleLevel
    }
    bunyan.debug('Debug message')
}

Controlling Pino

Here is a sample of how to use the Pino general purpose logger to respond to these environment variables in a Lambda function.

import Pino from 'pino'

let {LOG_FILTER, LOG_OVERRIDE, LOG_SAMPLE} = process.env

if (LOG_OVERRIDE != null) {
    let [expire, level] = LOG_OVERRIDE.split(':')
    if (level && Date.now() < expire) {
        LOG_FILTER = level
    }
}
let sample = 0, sampleRate, sampleLevel
if (LOG_SAMPLE != null) {
    [sampleRate, sampleLevel] = LOG_SAMPLE.split(':')
}

const pino = Pino({name: 'pino', level: LOG_FILTER})

exports.handler = async (event, context) => {
    if (sampleRate && (sample++ % (100 / sampleRate)) == 0) {
        pino.level = sampleLevel
    }
    pino.debug('Debug message')
}

Controlling Python

Here is a sample of how to use this technique with Python.

The stock Python logger supports only numeric log levels.

import os
import logging
from datetime import datetime

LOG_FILTER = os.environ.get('LOG_FILTER')
LOG_OVERRIDE = os.environ.get('LOG_OVERRIDE')
LOG_SAMPLE = os.environ.get('LOG_SAMPLE')

sample = 0
sampleRate = 0
sampleLevel = None

if LOG_SAMPLE != None:
    sampleRate, sampleLevel = LOG_SAMPLE.split(':')

if LOG_OVERRIDE != None:
    when, level = LOG_OVERRIDE.split(':')
    if level and int(datetime.today().timestamp()) < int(when):
        LOG_FILTER = level

logging.basicConfig(level = int(LOG_FILTER))
log = logging.getLogger()

def lambda_handler(event, context):
    sample = sample + 1
    if sampleRate and (sample % (100 / sampleRate)) == 0:
        log.setLevel(int(sampleLevel))

    log.info("Hello World")
    return 'done'

Controlling Winston

Here is a sample of how to use the Winston logger.

import Winston from 'winston'

let {LOG_FILTER, LOG_OVERRIDE, LOG_SAMPLE} = process.env

if (LOG_OVERRIDE != null) {
    let [expire, level] = LOG_OVERRIDE.split(':')
    if (level && Date.now() < expire) {
        LOG_FILTER = level
    }
}
let sample = 0, sampleRate, sampleLevel
if (LOG_SAMPLE != null) {
    [sampleRate, sampleLevel] = LOG_SAMPLE.split(':')
}

const winston = Winston.createLogger({
    level: LOG_FILTER,
    transports: [new Winston.transports.Console()],
})

exports.handler = async (event, context) => {
    if (sampleRate && (sample++ % (100 / sampleRate)) == 0) {
        winston.transports[0].level = sampleLevel
    }
    winston.debug('Debug message')
}

Links

Comments

{{comment.name}} said ...

{{comment.message}}
{{comment.date}}

Make a Comment

Thank You!

Messages are moderated.

Your message will be posted shortly.

Sorry

Your message could not be processed at this time.

Error: {{error}}

Please retry later.

OK