Saturday 12 March 2022

Write to Azure Blob Storage with Serilog in .NET Core

Similar to the previous post https://jaryl-lan.blogspot.com/2022/03/write-to-elasticsearch-with-serilog-in.html. Will be using Serilog library to write to Azure Blob Storage. Before we can start, you are required to have a Azure Storage Account up and running. Do not worry if you do not have an Azure account as you can actually run the emulator on your local machine. If you have Visual Studio 2022 installed on your machine, then Azurite (To replace Azure Storage Emulator) is included. Otherwise you will have to manually install it.

For those who has Visual Studio 2022. It is located at the following directory

Enterprise:
C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Microsoft\Azure Storage Emulator

Professional:
C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\IDE\Extensions\Microsoft\Azure Storage Emulator
For those does not have Visual Studio 2022 installed. 1 alternative is to run it with docker. You may be wondering on the ports defined. Port 10000 is for blob service, 10001 is for queue service and lastly 10002 is for table service.
Pull the azurite image:
docker pull mcr.microsoft.com/azure-storage/azurite

Run the azurite:
docker run --rm -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite

Other than the Azurite, you are required to install Microsoft Azure Storage Explorer to browse the logs written into the blob storage.

Back to the code, make sure the following 2 NuGet packages is included.
  • Serilog.AspNetCore
  • Serilog.Sinks.AzureBlobStorage
  • You may use 1 of the following methods to be implemented into your Program.cs file.

    Method 1: Configure Serilog in code.

    [.NET 6] - Minimal hosting model
    builder.Host.UseSerilog((hostBuilderContext, loggerConfiguration) =>
        loggerConfiguration
            .WriteTo.Console()
            .WriteTo.AzureBlobStorage(
                "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;",
                Serilog.Events.LogEventLevel.Information,
                storageContainerName: "writetoazureblobstoragewithserilog",
                storageFileName: "WriteToAzureBlobStorageWithSerilog-{yyyy}-{MM}-{dd}.txt"));
    SelfLog.Enable(Console.Error);

    [.NET 5 and earlier]
    Host.CreateDefaultBuilder(args)
        .UseSerilog((hostBuilderContext, loggerConfiguration) => {
            loggerConfiguration
            .WriteTo.Console()
            .WriteTo.AzureBlobStorage(
                "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;",
                Serilog.Events.LogEventLevel.Information,
                storageContainerName: "writetoazureblobstoragewithserilog",
                storageFileName: "WriteToAzureBlobStorageWithSerilog-{yyyy}-{MM}-{dd}.txt");
            SelfLog.Enable(Console.Error);
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

    Method 2: Configure Serilog from appSettings file.

    [appSettings.json]
    "Serilog": {
      "WriteTo": [
        {
          "Name": "Console"
        },
        {
          "Name": "AzureBlobStorage",
          "Args": {
            "ConnectionString": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;",
            "storageContainerName": "writetoazureblobstoragewithserilog",
            "storageFileName": "WriteToAzureBlobStorageWithSerilog-{yyyy}-{MM}-{dd}.txt"
          }
        }
      ]
    }

    [.NET 6] - Minimal hosting model
    builder.Host.UseSerilog((hostBuilderContext, loggerConfiguration) =>
        loggerConfiguration
            .ReadFrom.Configuration(hostBuilderContext.Configuration));
    SelfLog.Enable(Console.Error);

    [.NET 5 and earlier]
    Host.CreateDefaultBuilder(args)
        .UseSerilog((hostBuilderContext, loggerConfiguration) =>
        {
            loggerConfiguration
                .ReadFrom.Configuration(hostBuilderContext.Configuration);
            SelfLog.Enable(Console.Error);
        })

    Microsoft Azure Storage Explorer

    For more details on the setup and the ConnectionString, do refer this link https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio





    Sunday 6 March 2022

    Write to ElasticSearch 8 with Serilog in .NET Core

    Continuation from the previous post https://jaryl-lan.blogspot.com/2022/02/write-to-physical-file-with-serilog-in.html which talks about writing to physical file through Serilog. For this round will be to write logs to Elasticsearch with Serilog.

    First and foremost, you need to have Elasticsearch up and running. So if you do not have it, you will have to install it on your local machine. Otherwise ignore the installation steps. There are few ways to go about it but I will go through using docker command with minimal steps.

    There are 2 images required. Elasticsearch and Kibana. Kibana is required for you to view the logs.

    1. Run these commands to grab the Elasticsearch and Kibana image

    docker pull docker.elastic.co/elasticsearch/elasticsearch:8.0.1
    docker pull docker.elastic.co/kibana/kibana:8.0.1

    2. Run this command to create a network. To be used by Elasticsearch and Kibana late

    docker network create elasticsearch

    3. Run this command to start the Elasticsearch

    docker run --rm --name localelasticsearch --net elasticsearch -p 9200:9200 -p 9300:9300 -t docker.elastic.co/elasticsearch/elasticsearch:8.0.1

    Note: If the Elasticsearch stopped on its own, most likely it is due to error. If it shows the following error, you are then required to increase the max virtual memory.

    ERROR: [1] bootstrap checks failed. You must address the points described in the following [1] lines before starting Elasticsearch.
    bootstrap check failure [1] of [1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

    The command to set the maximum virtual memory is heavily depend on where the docker is installed. In my case the docker is on the wsl. So if is different, you may refer from this link to identify which command is suitable https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#_set_vm_max_map_count_to_at_least_262144.

    So for wsl, firstly terminal into the wsl with the following command.

    wsl -d docker-desktop

    Optional: you can use this command to check what is the current max virtual memory value. In my case it is showing 65530.

    cat /proc/sys/vm/max_map_count

    Run the following command to set the max virtual memory value to 262144

    sysctl -w vm.max_map_count=262144

    Once done, type exit and hit enter key to exit from the wsl terminal.

    exit

    Run this command again to verify that the Elasticsearch did not terminate on its own.

    docker run --rm --name localelasticsearch --net elasticsearch -p 9200:9200 -p 9300:9300 -t docker.elastic.co/elasticsearch/elasticsearch:8.0.1

    4. Run this command to start the kibana.

    docker run --rm --name localkibana --net elasticsearch -p 5601:5601 docker.elastic.co/kibana/kibana:8.0.1

    5. Open up your browser and browse to http://localhost:5601. It may asked you for a token. the token can be obtained from the console output that you run on step 3. The token should display below the following text

    Token for kibana

    6. After fill up the token. The portal will request for 6 digit code, which printed on the console that runs the kibana in step 4. Look for the keyword "Your verification code is:".

    7. Finally, it will asked you to fill up the login credentials. The username is elastic, whereas the password is printed on the console output that runs the elasticsearch in step 3.

    Elastic password

    With this, you have completed the setup of Elasticsearch and kibana on your local machine. For more details on the setup steps, you may refer to this link https://www.elastic.co/guide/en/kibana/current/docker.html.

    With the environment ready to use. Let's get back to the code. 

    The NuGet packages requires are as follows

    • Serilog.AspNetCore
    • Serilog.Sinks.Elasticsearch
    Head to the Program.cs file and configure the UseSerilog().

    [Configure Serilog in code]
    Note 1: The username and password is the same credentials used to login into kibana.
    Note 2: As of this writing, the Serilog.Sinks.Elasticsearch is having issue writing to Elasticsearch version 8. You will get hit with the following error due to the deprecation of type parameter.

    Action/metadata line [1] contains an unknown parameter [_type]

    For more detail, do refer on this link https://github.com/serilog-contrib/serilog-sinks-elasticsearch/issues/375. As mention in the link, we will need to add TypeName = null. Also due to this issue, we can't use the configuration way to configure writing logs into Elasticsearch.

    [.NET 6] - Minimal hosting model
    builder.Host.UseSerilog((hostBuilderContext, loggerConfiguration) =>
        loggerConfiguration
            .WriteTo.Console()
            .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("https://yourusername:yourpassword@localhost:9200"))
            {
                TypeName = null,
                AutoRegisterTemplate = true,
                IndexFormat = "WriteToElasticsearchWithSerilog-{0:yyyy-MM-dd}",
            }));
    SelfLog.Enable(Console.Error);

    [.NET 5 and earlier]
    .UseSerilog((hostBuilderContext, loggerConfiguration) => {
        loggerConfiguration
            .WriteTo.Console()
            .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("https://yourusername:yourpassword@localhost:9200"))
            {
                TypeName = null,
                AutoRegisterTemplate = true,
                IndexFormat = "WriteToElasticsearchWithSerilog-{0:yyyy-MM-dd}",
            });
        SelfLog.Enable(Console.Error);
    })

    The reason of adding the SelfLog.Enable(Console.Error); is to print out any errors that happens when writing logs to Elasticsearch. Which helps in troubleshooting. Also you may consider changing it to write into physical file or any other storage.

    If you not able to log to Elasticsearch and received the following error:

    Caught exception while preforming bulk operation to Elasticsearch: Elasticsearch.Net.ElasticsearchClientException: The SSL connection could not be established, see inner exception..
    Call: Status code unknown from: POST /_bulk
     ---> System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
     ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot

    It means that you do not have the cert used by the Elasticsearch since Elasticsearch create its own cert (Self-Signed Certificate) when it launched the first time. There are ways to modify your code to forcefully ignore certificate error. In this case we are not going into that route. But instead add the certificate into the "Trusted Root Certification Authorities" store.

    Before we can add it to the certificate store, we need to grab the certificate generated by the Elasticsearch. In the official link https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#elasticsearch-security-certificates. it specified /usr/share/elasticsearch/config/certs, which is the path of the certificate generated in the Elasticsearch container and we need the file http_ca.crt. To grab it out to your physical folder, run the following command.

    Command argument to copy file from container to physical folder:

    docker cp <Elasticsearch container name>:<file path in container> <Physical file path>
    Sample command to copy out the http_ca.crt to c drive:
    docker cp localelasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt C:/http_ca.crt

    Once the file has been copied out. Locate the certificate file and run it.

    1. Click Install Certificate... button.

    Certificate
    2. Maintain as Current User and click Next button.
    Certificate Import Wizard
    3. Select Place all certificates in the following store and click Browse... button.
    Certificate Import Wizard
    4. Select Trusted Root Certification Authorities and click OK button
    Certificate Import Wizard
    5. Click Next button. On the next screen click Finish button.
    Certificate Import Wizard

    6. If it prompt a Security Warning window, click Yes button.

    Once the certificate has been added to the certificate store. Rerun your application and the error will no longer appear.

    Kibana

    If you need to rely on configuration file instead of code to configure, probably advisable to use version 7 instead due to the library yet to properly support version 8 of Elasticsearch. Do follow up from their github issue https://github.com/serilog-contrib/serilog-sinks-elasticsearch/issues.

    Sample Code: https://1drv.ms/u/s!Aj2AA7hoIWHm1HCLWNJsI6ZrS1Sb?e=TnyW0T