Serilog and CloudWatch (with inbuilt credentials)

In this post we’ll look at the best way to get Serilog entries to push to Cloudwatch in the most unobtrusive way.

Serilog is the defacto standard for logging in dotnet core. It provides integration with the ILogger interface, along with supports structured logging. Beyond that integration, it has extensive support for a multitude of different logging/error providers, from Raygun to Seq, from Datadog to ElasticSearch.

Cloudwatch is the monitoring service built into AWS. It provides logging, metrics and more recently added Event Tracing in the form of X-Ray. We’ll be specifically looking at the logging side of Cloudwatch which can accept text based logs, as well as structured logs in the form JSON which can then be queried using a JSON query syntax.

Additionally, we’ll include how to use IAM roles to securely allow the application to access the logs.

Pre-requistes:

  • AWS Account with admin privileges
  • Deployed API solution to an AWS EC2 instance

What we’ll setup here is:

  • Pushing logging to Cloudwatch when an appsetting has been set giving it a LogGroup
  • Using inbuilt credentials to access cloudwatch
  • Additionally writing to the console

First lets add some packages that we’ll need:

dotnet add package Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.AwsCloudWatch
dotnet add package AWSSDK.CloudWatchLogs
dotnet add package AWSSDK.Extensions.NETCore.Setup

Lets add a settings object to allow our logging settings to structured in appsettings.json

    public class LoggingSettings
    {
            public string CloudWatchLogGroup { get; set; }
    }

In order to make the code more readable, we’ll encapsulate the setup method. This is the method that will do all work in getting things setup.

  
    public static class Logging
    {
        public static void SetupAWSLogging(WebHostBuilderContext hostingContext, LoggerConfiguration loggerConfiguration)
        {
            var config = hostingContext.Configuration;
            var settings = config.GetSection("LoggingSettings").Get<LoggingSettings>();

            loggerConfiguration
                .MinimumLevel.Information()
                .Enrich.FromLogContext()
                .WriteTo.Console();

            if (!string.IsNullOrEmpty(settings.CloudWatchLogGroup))
            {
                var options = new CloudWatchSinkOptions
                {
                    LogGroupName = settings.CloudWatchLogGroup,
                    CreateLogGroup = true,
                    MinimumLogEventLevel = LogEventLevel.Information,
                    TextFormatter = new CompactJsonFormatter()
                };
                var awsOptions = config.GetAWSOptions();
                var cloudwatchClient = awsOptions.CreateServiceClient<IAmazonCloudWatchLogs>();
                loggerConfiguration
                    .WriteTo.AmazonCloudWatch(options, cloudwatchClient);
            }

        }
    }

Lets look a bit closer at what we’re doing here.

var config = hostingContext.Configuration;
var settings = config.GetSection("LoggingSettings").Get<LoggingSettings>();

This allows us to get access to the appsettings, or anything in the configuration change (Environment Variables, SystemsManager settings etc. We’ll then get our settings object.

loggerConfiguration
       .MinimumLevel.Information()
       .Enrich.FromLogContext()
       .WriteTo.Console();

This sets up our defaults that will apply to all the places we will send the logs, then also sets up console logging regardless of whether or not Cloudwatch is enabled.

var options = new CloudWatchSinkOptions
{
    LogGroupName = settings.CloudWatchLogGroup,
    CreateLogGroup = true,
    MinimumLogEventLevel = LogEventLevel.Information,
    TextFormatter = new CompactJsonFormatter()
};
var awsOptions = config.GetAWSOptions();
var cloudwatchClient = awsOptions.CreateServiceClient<IAmazonCloudWatchLogs>();
loggerConfiguration
    .WriteTo.AmazonCloudWatch(options, cloudwatchClient);

This is where the “Magic” happens. First we use our settings to find which CloudWatch LogGroup to use, then we’ll apply the “CompactJsonFormatter” to the output. This is the one that will allow us to search the logs easier. If you emit this, you’ll get text based logs instead.

Finally, we’ll use the special “GetAWSOptions()” method the NETCore setup package for AWS to get access to the credentials to use. This is an uber-special method as it will chose the most appropriate AWS Credentials based on a chain of different things, this could be set in your appsettings.json, in a profile on your computer, an IAM role applied to your service. You don’t need to care within this method.

Once we have these setup, we can tell our WebHostBuilder to use it. If you’re using the default netcoreapp3.1 template, your setup will look something like this:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>()
                .UseSerilog(SetupAWSLogging);
        });

The “SetupAWSLogging” may need prequalifying with the class name if it’s not in the same class.

The last thing to do is add the settings to our appsettings.json

{
  "LoggingSettings": {
    "CloudWatchLogGroup": ""
  },
  "AWS": {
    "Profile": "Default",
    "Region": "eu-west-1"
  }
}

If you run your application without the setting applied, you will just get console logging. If you add in a valid Cloudwatch LogGroup, and also setup an AWS profile on the machine that has the right access, you will should see something like this in CloudWatch:

On clicking through, you should see something resembling the below:

Hopefully this should show you how easy it is to integrate cloudwatch logs with your application. Cloudwatch is a great tool if you’re integrated into the AWS eco-system. If you’re using anything that is integrated with IAM roles (EC2, ECS, Lambda, etc.) then this is a great way to integrate with inbuilt AWS authentication.

IAM Policy
Here’s an IAM policy that will let your user access the desired group, and write the necessary streams. (Remember to replace “cloudwatch-test” with your desired log-group)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "LogStreams",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:DescribeLogStreams",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:cloudwatch-test:log-stream:*"
        },
        {
            "Sid": "LogGroups",
            "Effect": "Allow",
            "Action": [
                "logs:DescribeLogGroups"
            ],
            "Resource": "arn:aws:logs:*:*:log-group:cloudwatch-test"
        }
    ]
}

7 thoughts on “Serilog and CloudWatch (with inbuilt credentials)

Add yours

  1. Thank you for sharing a simple way to integrate logs with the AWS cloudwatch. Can you please elaborate on how to setup AWS profile, or share any reference so that I can implement and verify serilog integration on cloudwatch. Thanks

    Like

    1. Unfortunately I don’t have access to the IAM profile. Granting Log privileges to an EC2 isn’t easy, but the main thing to remember is to add access to the Log AND the LogStream. Play around with the asterisks in the LogStream, takes a bit of getting used to.

      Like

  2. Hi Martin. Thanks for the tutorial. Any chance you could go into a bit more detail regarding the IAM policy creation in this usage, or provide a good resource to enable EC2 to CloudWatch communication, please? I’m having a hard time getting that last part to “click”. I can however confirm that console and text logs are working using your example so this appears to be a matter of authorization.

    Like

    1. I haven’t got a policy to hand, and I’ve actually moved away from having all this AWS code. The key is to apply the role to the EC2 instance, and ensure that it has the permissions on the Log AND the LogStream. LogStream permissions are… problematic, and getting the stars in the right place is quite hard.

      Sorry I couldn’t be more help.

      Like

    1. Sinks in Serilog are for outputting log entries sent to serilog into a particular datasource. That is to say that a Sink is a destination that logs go to. The Logger package you’re looking at is to push Logs from the AWS.Logger (mostly used in Lambda if I remember correctly) into Serilog.

      So, to get Lambda Logs into Cloudwatch, you need both packages… the AWS Logger pushed the logs to Serilog, Serilog then formats them and sending them to the Sink for Cloudwatch.

      Like

Leave a comment

Website Built with WordPress.com.

Up ↑