Skip to main content

Instrumentation, logging and debugging applications

Overview

In software engineering, logging and instrumentation are two different techniques used to monitor and debug applications.

Logging is the process of recording events that occur in an application. These events can include error messages, warnings, and other information that can be used to diagnose problems. Logging is typically used to capture information about what has happened in the past. It is a way to store data that can be analysed later to understand what went wrong.

Instrumentation, on the other hand, is the process of adding code to an application to collect data about its behaviour. This data can include metrics, events, logs, and traces. Instrumentation is used to capture information about what is happening in real-time. It is a way to monitor an application and understand how it is performing. Instrumentation also offers a way to easily provide cross process and domain boundaries,

[!Note] This document does not provide an indepth look at instrumentation prinipals but instead concentrates on how to output instrumentation using the Variant platform.

Azure Application Insights for data collection

Once information has been collected it has to be stored or logged. Variant uses Azure's monitoring services Application Insights to store this data and is normally automatically configured when deploying applications using the app settings: APPLICATIONINSIGHTS_CONNECTION_STRING. This can be overridden in your settings to point to any App Insight instance in any Azure subscription.

Instrumentation

Adding instrumentation using activities

Activities are core to adding instrumentation to applications and as the storage of logs and instrumentation is already configured in the application, in Variant its as simple as adding the ACTIVITY_NAME setting to either a connector or pipe. Doing this provides us with 3 different types of instrumentation categories:

  • Basic instrumentation
  • Domain specific instrumentation
  • Http instrumentation

Basic Instrumentation

If we look at the following we can see that all we have done is added the activity name to the end point and 2 pipes.

In Application insights the following information is stored:

With the Json.ConvertAndV alidateRequest storing the basic information

Although this is helpful for flow it can be improved slight by adding additional information

Adding Domain specific Information

Adding custom properties is done via the Variant.Core.UpdateActivityTraceStrategyPipe with the properties seen below

      - pipe: Variant.Core.UpdateActivityTraceStrategyPipe
       # Allows the storage of an activities status code
        ACTIVITY_STATUS_CODE: null
        # These are the custom properties and are stored as key/value pairs
        TAGS: null
        # This is the baggage and again stored as key/value pairs
        BAGGAGE: null

If we take the previous example if might be useful if we stored the request and response data as well. This can be done by adding the following pipe to the end of the pipeline:

  - pipe: Variant.Core.UpdateActivityTraceStrategyPipe
TAGS: 
chat: ${Request.chat}
id: ${?Request.id}
responseChat: ${Response.response}
responseId: ${Response.id}

This then gives us the following information in the logs:

[!Note] The above example shows us the benefits of instrumentation. As we can see that the actual call took over 28 seconds to return. If we look at the output we can see it was the call to ChatGpt which took the majority of the time.

Adding Http instrumentation

Although we now have good domain specific instrumentation we can provide extra information on every HTTP call that is made during our request. To do this we need ensure the 'Variant.Instrumentation.AddHttpClient' setting is set to true as below (default is false):

Now when we call the service we get the following trace created:

This now shows that are out ChatGPT call performs 2 Http client calls to booth the ChatGPT API endpoint and then to an Azure Storage blob container to store the conversation. If we add an id to the request we can see that it first makes a call to return the conversation from storage before calling the endpoint :

Logging and debugging

In Variant there are 2 pipes whcih are used for logging and debugging applications. These are:

  • Variant.Core.LoggerPipe
  • Variant.Core.LogDiagnosticsPipe

Variant.Core.LoggerPipe

This pipe, as well as it's derivative pipe 'Variant.Core.LogMessagePipe', are used to log any information and at any logging level to the logs. These can be placed anywhere and can log any data found in the current message. Their implementations are:

	#
# Prefer to use the Variant.Core.LogMessagePipe instead
# as this pipes signiture is built from a .NET implmentation
#
- key: Variant.Core.LoggerPipe
value:
type: Variant.Strategies.Core.LoggerPipe, Variant.Strategies.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
replacements:
LOG_MESSAGE:
LOG_MESSAGE_PARAMETERS:
INSTANCE_NAME:
ACTIVITY_NAME:
IS_ENABLED:
LOG_LEVEL: Debug

- key: Variant.Core.LogMessagePipe
value:
pipe: Variant.Core.LoggerPipe
replacements:
LOG_MESSAGE: null
LOG_MESSAGE_PARAMETERS: []
LOG_LEVEL: Debug

The main settings of these pipes are:

The LOG_MESSAGE setting can be any string value and can contain substitutions The LOG_MESSAGE_PARAMETERS is an array of string values that allows data collectors i.e. App Insights, to store these parameters defined in the LOG_MESSAGE separately to the message. This allows log messages to be searched and filtered based on the parameter values.
The LOG_LEVEL determines what to log based on the applications Logging level. The allowed values are:

  • Trace
  • Debug
  • Information
  • Warning
  • Error
  • Critical

[!Tip] Wherever possible use the LOG_MESSAGE_PARAMETERS in conjunction with the LOG_MESSAGE

Here are a couple of examples on how to log message with and without the message parameters setting:

     - pipe: Variant.Core.LogMessagePipe
       LOG_LEVEL: Debug
       LOG_MESSAGE: The current time is ${DateTime.Now}

       # Note when uses the parameters setting there is no '$' prefix before the '{'
       # This is because its not a Variant substitution value but Microsoft's logger library  
     - pipe: Variant.Core.LogMessagePipe
       LOG_LEVEL: Debug       
       LOG_MESSAGE: The current time is {CurrentTime}
       LOG_MESSAGE_PARAMETERS: 
        - ${DateTime.Now}

Variant.Core.LogDiagnosticsPipe

This pipe, as well as it's derivatives are used purely for debugging when developing applications. Its use is for outputting the current message headers and body (if required) to either the console or, in the case of an API call as the response stream.

The values of this pipe and its derivatives are as follows:

    # Prefer to use the derivitives of this pipe rather than this pipe itself
- key: Variant.Core.LogDiagnosticsPipe
value:
type: Variant.Strategies.Core.LogDiagnosticsPipe, Variant.Strategies.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
replacements:
INSTANCE_NAME:
ACTIVITY_NAME:
IS_ENABLED:
LOG_PAYLOAD: False
LOG_MESSAGE_TO_CONSOLE: False
SET_RESPONSE_TO_MESSAGE: False

- key: Variant.Core.ReturnMessageLogPipe
value:
pipe: Variant.Core.LogDiagnosticsPipe
IS_ENABLED: ${Variant.Environment.Type} == "dev"
LOG_PAYLOAD: true
SET_RESPONSE_TO_MESSAGE: true


- key: Variant.Core.LogToConsolePipe
value:
pipe: Variant.Core.LogDiagnosticsPipe
IS_ENABLED: ${Variant.Environment.Type} == "dev"
LOG_PAYLOAD: False
LOG_FOR_CONSOLE: true

[!Note] Both the Variant.Core.ReturnMessageLogPipe and Variant.Core.LogToConsolePipe are removed from all pipelines if the environment type is not dev

Variant.Core.ReturnMessageLogPipe

When developing API endpoints there may be times where you need to know what the state of the message is, or what variables are available, at a certain time. If you add the following pipe to any part of the message the pipeline will stop processing that message and return the message values as the Response object immediately:

  - pipe: Variant.Core.ReturnMessageLogPipe

The return value of the any process calling this API now recieves the contents something like this:

Variant Message Diagnostic Data:
    MessageId:    b97a20b5-2bc2-418b-971d-35431eaa527c
    ThreadId: 17
    PartCount: {
    {
        0
    }
}1
    UniteMessage Context
    _ApiValidators : System.Collections.Generic.List`1[Variant.Strategies.Core.ValidatorItem
]
    Defaults       : 
    Message.Id     : b97a20b5-2bc2-418b-971d-35431eaa527c
    Request.Header.Accept: */*
    Request.Header.Accept-Encoding: System.String[]
    Request.Header.client-ip: 90.152.4.252:10202
    Request.Header.Content-Length: 115
    Request.Header.Content-Type: text/plain
    Request.Header.disguised-host: com-lokidemo2-new-lmk9vxn0-fa.azurewebsites.net
    Request.Header.Enumerator: System.Net.Http.Headers.HttpHeaders+<GetEnumeratorCore>d__24
    Request.Header.Host: com-lokidemo2-new-lmk9vxn0-fa.azurewebsites.net
    Request.Header.Max-Forwards: 10
    Request.Header.Method: POST
    Request.Header.postman-token: 76ac55b1-dd6f-438a-adf9-35c14c5ead88
    Request.Header.Scheme: https
    Request.Header.User-Agent: PostmanRuntime/7.35.0
    Request.Header.was-default-hostname: com-lokidemo2-new-lmk9vxn0-fa.azurewebsites.net
    Request.Header.x-appservice-proto: https
    Request.Header.x-arr-log-id: 87c0e0f6-a98a-4b00-9fec-79056f6aea34
    Request.Header.x-arr-ssl: 2048|256|CN=Microsoft Azure TLS Issuing CA 01, O=Microsoft Corporation, C=US|CN=*.azurewebsites.net, O=Microsoft Corporation, L=Redmond, S=WA, C=US
    Request.Header.x-forwarded-for: 90.152.4.252:10202
    Request.Header.x-forwarded-proto: https
    Request.Header.x-forwarded-tlsversion: 1.2
    Request.Header.x-original-url: /api/test
    Request.Header.x-site-deployment-id: com-lokidemo2-new-lmk9vxn0-fa
    Request.Header.x-waws-unencoded-url: /api/test
    Request.Property.AbsolutePath: /api/test
    Request.Property.LocalPath: /api/test
    Request.Property.Url: https://com-lokidemo2-new-lmk9vxn0-fa.azurewebsites.net/api/test
    Request.Query  : 

 -------------------- 'Primary' Details----------------------------------
    PartName: 'Primary' ContextEmpty
    PartName: 'Primary        ' Data type: System.IO.MemoryStream
    PartName: 'Primary        ' Data: {{   
    "id": "chatcmpl-8KlyVs3en51eFFj8CgYNB7Rq8Hnkg",
    "chat":"provide explainations of each name"
}}
 ----------------- End of 'Primary' Details-------------------------------

Variant.Core.LogToConsolePipe

If the call is not an API or you want to allow the call to continue you can use this pipe as follows and will output the same diagnostics data as above to the logs:

  - pipe: Variant.Core.LogToConsolePipe
LOG_PAYLOAD: false

However, care must taken when setting the LOG_PAYLOAD setting to true as the pipe will automatically convert the payload to a string and it will remain that type for all following calls. A example of this is that if the payload was a Stream and a stream was expected in a following pipe then an error would be raised.