The Lambda Blog

Serverless Cloud Guide

Menu
  • Home
  • Categories
  • Projects
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact
Menu
coding a json format logger in python for use as a lambda layer package

Coding a JSON format logger in Python for use as a Lambda Layer package

Posted on May 24, 2022August 14, 2022 by user
Navigation » Home » CloudWatch

Lambda Layers are used for more than just making shared classes, packages and libraries available across your Lambda functions. You can for instance share a custom runtime for your serverless Lambda function to execute in if your default AWS supplied runtime is insufficient for your particular function. But sharing common utilities and other common code like custom objects or configurations are probably the most common use case with Lambda layers. This article provides such a sample utility – a JSON format based logging class in the Python programming language that makes use of the excellent json-logging library and adds a little custom functionality like timing calculations.

I personally use versions of this coding example by default for logging in most of my Lambdas and is usually a part of my CloudFormation stack globals

Globals:
  Function:
    Runtime: python3.9
    Layers:
      - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:utility-cloudwatch-json-logger:1"

This way, I have access to the layer function to log in JSON format across my Lambdas in the stack and using it is quite easy as I will demonstrate in this article.

Jump to the section below to dive straight into the implementation details if you know all about the benefits of logging in a JSON format, but I do want to discuss the reasons why first for the benefits of anyone interested.

Why log in a JSON format (and why you really should always do this!) ?

To touch on some commonly understood points about logging – it is generally necessary, it can help identify problems, aid debugging, help investigate events, trace workflows – a whole load of stuff. There are philosophical arguments on how much logging one should and should not do and other debates and considerations about logging best practices and all that – I have my own opinions of course but that is not being discussed here.

There are two realities with logs with relation of cloud based micro service architectures as most serverless systems typically have-

1. There is usually a lot of logs – even when there isn’t a lot of activity, this design by its nature triggers a flow across multiple discrete service parts

2. Correlating and tracking logs across all these systems is necessary – it is typically not possible to manually track flows across individual logs beyond a certain scale.

And even if you have a monolithic application – depending on the number of functions within it and volume, manually tailing a log can be a nightmare.

Fortunately, the solutions for these problems of Log Collation, and searching across all of them, perhaps even with nice visual dashboards and graphs and all are many – look at OpenSearch with its Kibana dashboard or perhaps DataDog – both which I plan to be writing about in the near future – but that is a separate discussion.

Now what do all of these solutions really expect to make the most of their best features – collated, indexed and searchable logs from which you can build out dashboards and reports and all?

JSON Format Based Logging

That is correct – all of these services that uses underlying frameworks to index your logs require JSON formats to work. Even if your logs are not generated in JSON format by default – there is a necessary transformation step ( the LogStash part of the famous ELK stack acronym if you are familiar) before they are fully consumed.

While there are great tools – you can bypass the hassle of building and maintaining transformation logic by directly logging your data in a JSON formation to begin with! You can define indexable tags, for example – order id/service name in an ecommerce application and trace the id across your services of workflows.

Hypothetically, this would look like this…

logger.log(f"New Order:{order_id}", ORDER_ID=order_id, SERVICE_NAME="OrderEntry")
...
...
logger.log(f"Order Shipped:{order_id}", ORDER_ID=order_id, SERVICE_NAME="ShipProcessor")
..
..
logger.log(f"Order Billed:{order_id}", ORDER_ID=order_id, SERVICE_NAME="Billing")

And now you can have easy traceability using a service of your choice without any additional difficulty other than using log statements you probably would use anyway. The only minor additional burden is defining those tags you want to search with.

This is a beneficial no matter your environment – but this article will demonstrate a tool specifically for working with Lambdas from where the logs can be shipped from CloudWatch to a service of your choice.

A quick word on Lambda Layers and deploying them

This is not covering the basics of Lambda layers – its basically a deployed package available to your Lambdas (depending on the runtime) that can be used. A logger library like the one I am going to demonstrate is a great example for use as Lambda layer as all the Lambdas can simply reference it as use it – like I demonstrated above using the CloudFormation Globals property to make a particular jsong logger layer available.

A Lambda layer can be coded a zipped and manually deployed via the console – or if you prefer to have Infrastructure-as-Code template definitions:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: JSON Format Logger

Resources:
  UtilCloudwatchJsonLogger:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: utility-cloudwatch-json-logger
      ContentUri: ./utility-cloudwatch-json-logger
      CompatibleRuntimes:
        - python3.9
    Metadata:
      BuildMethod: python3.9

Which can then be built and deployed.

Now to code the layer in Python

Coding the JSON Format Logger Layer

First – a word on what this utility does on top of what we can use out of the box with the really excellent json logging library that it is using.

We want to start the function with a couple of required fields: Service Name & Application Name – so we know the service which is part of a larger application. If not, they can be the same name. The utility will not supply these indexable tags automatically and we do not need to log it all the time and will log the start generically for you

We want to end the function – again without writing generic code statements – the utility will log it for you.

We want to calculate some metrics – While CloudWatch collects metrics on a per Lambda level – this offers some granularity to automatically time blocks within your Lambda.

We want to log exceptions and of course regular logs.

This can of course be easily extended as suits your particular application especially with regards to additional helpers and log levels.

Lets code this out in Python.

import logging
import json_logging
import time
from datetime import datetime

class Logger:

    def __init__(self,
                 service_name: str,
                 app_name: str
                 ):

        self._logger = logging.getLogger(service_name)
        self._logger.setLevel(logging.DEBUG)
        self._logger.addHandler(logging.StreamHandler())

        json_logging.init_non_web(enable_json=True)

        self.service_name = service_name
        self.app_name = app_name
		
        #This is for sending custom index tags like order_id
        self.props = dict()
        
        #This is for calculating timed operations
        self.op_stats = dict()

        self.props["APP_NAME"] = app_name
        self.props["SERVICE_NAME"] = service_name

        self._logger.info(f"Logger for Service {service_name} of {app_name} initialized.", extra={"props": self.props})

    def log(self, message: str, **kwargs):
        self._logger.info(message, extra={"props": dict(**self.props, **kwargs)})

    def log_exception(self, message: str, exception: Exception, **kwargs):
        self._logger.exception(message, exc_info=exception, extra={"props": dict(**self.props, **kwargs)})

    def log_start(self):
        
        self._logger.info(f"Module {self.service_name} of {self.app_name} "
                          f"started processing.", extra={"props": self.props})

    def log_end(self):

        self._logger.info(f"Module {self.service_name} of {self.app_name} "
                          f"stopped processing.", extra={"props": self.props})

        self.props = dict()
        self.op_stats = dict()
        

    def log_op_start(self, op_id: str, **kwargs):
        self.op_stats[op_id] = time.perf_counter_ns()
        op_start = datetime.now().isoformat()
        self._logger.info(f"Tracking {op_id}",
                          extra={"props": dict(**self.props,
                                               op_id=op_id,
                                               op_start=op_start,
                                               **kwargs)})

    def log_op_end(self, op_id: str, op_name: str, **kwargs):
        operation_end_time = datetime.now().isoformat()
        if op_id in self.op_stats.keys():
            end_time = time.perf_counter_ns()

            start_time = self.op_stats[op_id]

            duration_nanos = end_time - start_time
            duration_millis = duration_nanos / 1000000
            duration_secs = duration_millis / 1000

            self._logger.info(f"{op_id} completed.",
                              extra={"props": dict(**self.props,
                                                   op_id=op_id,
                                                   op_start_time=start_time
                                                   op_end_time=end_time,
                                                   op_duration_millis=duration_millis,
                                                   op_duration_secs=duration_secs,
                                                   op_duration_nanos=duration_nanos,
                                                   **kwargs)})

This code should be self explanatory however rather than review each method – I think it is best demonstrated by an example with a Lambda.

Using the JSON Format Logger Layer in a Lambda

Now I have already demonstrated how to create a layer and reference via cloudformation.

If using the console – simply navigate to the Layers section of your Lambda and select it from the dropdown.

Note of course the runtime needs to be compatible – in this case Python.

Now within your code, using it is very simple.

from utility_cloudwatch_json_logger import Logger

logger = Logger(service_name="SampleLambda", app_name="SampleApplication")


def lambda_handler(event, context):

    logger.log_start()

    try:
		order_id = "Test123"
        
        logger.log(f"Yet Another Order:{order_id}", ORDER_ID=order_id)
		
        log_op_start(op_id="op1")
        
        logger.log(f"Doing Operation on Order:{order_id}", ORDER_ID=order_id)
        
        log_op_end(op_id="op1")

    except Exception as e:
        logger.log_exception("BOOM!", exception=e)
    finally:
    	logger.log_end()

And thats that.

Where from here?

Once you have a sample Lambda logging in Python format – you aren’t going to see any immediate benefits if you are simply looking at CloudWatch.

We need to ship these into an OpenSearch index or some other service like DataDog.

These will be future articles on this blog – but this covers the basics for that next step.

Recent Posts

  • Coding a JSON format logger in Python for use as a Lambda Layer package
  • Configuring an S3 Bucket to send events to a Lambda destination for processing
  • How to request a public SSL certificate for a domain name from the AWS Certificate Manager Console
  • Creating automated CloudFormation Stack Build and Deployments with AWS CodePipeline and CodeBuild
  • A concise guide to setting up the AWS command-line libraries on your local development environment
  • How to implement a Lambda Authorizer for an AWS AppSync API and invoke the API with the required Authorization Token
  • Filtering CloudWatch Logs by LogGroups and LogStreams and reading them using Python and the Boto3 SDK
  • Azure AD Multi Tenancy issue in AWS Cognito
  • Setting up Enterprise Federation from Azure Active Directory to Amazon Cognito using Open ID Connect
  • How to Setup IAM Multifactor Authentication (MFA) for the AWS CLI

Categories

  • Amplify
  • API Gateway
  • AppSync
  • AWS CLI
  • CloudFormation
  • CloudWatch
  • Cognito
  • DynamoDB
  • EventBridge
  • KMS
  • Lambda
  • Projects
  • Route 53
  • SES
  • SNS

Post Tags

ACM Amplify API Gateway AppSync AWS CLI Azure Boto3 CloudFormation CloudWatch CodeBuild CodePipeline Cognito DynamoDB EventBridge Firebase IAM KMS Lambda OIDC Project Python Rekognition Route53 S3 SAM SES SNS VPC

©2022 The Lambda Blog