Collecting Cosmos DB dependency telemetry with Application Insights
Tags: azure, cosmosdb, dotnet, performance
Published: 2022-02-15 17:29:45

In this article, we will use Application Insights to collect the requests made to Azure Cosmos DB so that we can easily understand the performance issues.

Cosmos DB is very fast, but depending on the query to be executed, it can consume a lot of Request Units or worsen the latency in some cases.

In order to understand the problem at the application level, you can create an extension to Cosmos DB SDK v3 and send the request information to Application Insights.

{% github Azure/azure-cosmos-dotnet-v3 %}

Cosmos DB SDK v3 provides an extension point called `RequestHandler`, which can be used to implement the Application Insights dependency collector.

It is very easy to send dependency telemetry to Application Insights. Please refer to the official documentation available here.

- [Track custom operations with Azure Application Insights .NET SDK - Azure Monitor | Microsoft Docs](https://docs.microsoft.com/en-us/azure/azure-monitor/app/custom-operations-tracking)

The `RequestHandler` to send dependency telemetry to Application Insights that we implemented is as follows.

Once you have decided what to send to Application Insights, the sending itself can be done with a very simple code. In this example, we are sending the name of the resource in Cosmos DB and the Request Units that were consumed.

```csharp
public class AppInsightsRequestHandler : RequestHandler
{
    public SimpleAppInsightsRequestHandler(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    private readonly TelemetryClient _telemetryClient;

    public override async Task<ResponseMessage> SendAsync(RequestMessage request, CancellationToken cancellationToken)
    {
        using var dependency = _telemetryClient.StartOperation<DependencyTelemetry>($"{request.Method} {request.RequestUri.OriginalString}");

        var response = await base.SendAsync(request, cancellationToken);

        var telemetry = dependency.Telemetry;

        // Used to identify Cosmos DB in Application Insights
        telemetry.Type = "Azure DocumentDB";
        telemetry.Data = request.RequestUri.OriginalString;

        telemetry.ResultCode = ((int)response.StatusCode).ToString();
        telemetry.Success = response.IsSuccessStatusCode;

        // Send with Metrics
        telemetry.Metrics["RequestCharge"] = response.Headers.RequestCharge;

        return response;
    }
}
```

To use the implemented `RequestHandler`, you just need to write the following code with Dependency Injection. Don't forget to install the Application Insights SDK for your application.

```csharp
builder.Services.AddSingleton<AppInsightsRequestHandler>();

builder.Services.AddSingleton(provider =>
{
    var requestHandler = provider.GetRequiredService<AppInsightsRequestHandler>();

    var connectionString = builder.Configuration.GetConnectionString("CosmosConnection");

    return new CosmosClient(connectionString, new CosmosClientOptions
    {
        CustomHandlers = { requestHandler }
    });
});
```

If you run the application with the above code, you will be able to check the Cosmos DB request information from Dependency in Application Insights.

![Dependency performance in Application Insights](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c55tn2znl84jx9jqizwz.png)

The telemetry details can be viewed from the E2E transaction view to see the Request Units consumed by the request and the resource URIs executed.

![Dependency details in Application Insights](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p42ijxdeqq6iaw6zqows.png)

Request Units that are sent at the same time as dependency telemetry are treated as metrics and can be displayed graphically from the Metric view of Application Insights. Of course, advanced aggregation using KQL can also be done easily.

The simple implementation so far provides a minimum of information, but we will make the information to be sent more detailed.

The Application Insights SDK implements a dependency collector for Cosmos DB, but it is only available when the connection mode is Gateway. However, the implementation can be reused, so we will refer to it to detail the telemetry.

- [ApplicationInsights-dotnet/DocumentDbHttpParser.cs at main · microsoft/ApplicationInsights-dotnet · GitHub](https://github.com/microsoft/ApplicationInsights-dotnet/blob/main/WEB/Src/DependencyCollector/DependencyCollector/Implementation/HttpParsers/DocumentDbHttpParser.cs)

The following is an implementation that uses the Application Insights SDK implementation to flesh out the telemetry.

Some methods that are not related to Cosmos DB are omitted, so please copy them from the Application Insights SDK if necessary.

```csharp
public class AppInsightsRequestHandler : RequestHandler
{
    public AppInsightsRequestHandler(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    private readonly TelemetryClient _telemetryClient;

    public override async Task<ResponseMessage> SendAsync(RequestMessage request, CancellationToken cancellationToken)
    {
        using var dependency = _telemetryClient.StartOperation<DependencyTelemetry>("Cosmos");

        var response = await base.SendAsync(request, cancellationToken);

        var telemetry = dependency.Telemetry;

        var resourcePath = HttpParsingHelper.ParseResourcePath(request.RequestUri.OriginalString);

        // populate properties
        foreach (var (key, value) in resourcePath)
        {
            if (value is not null)
            {
                var propertyName = GetPropertyNameForResource(key);
                if (propertyName is not null)
                {
                    telemetry.Properties[propertyName] = value;
                }
            }
        }

        var operation = HttpParsingHelper.BuildOperationMoniker(request.Method.ToString(), resourcePath);
        var operationName = GetOperationName(operation);

        telemetry.Type = "Azure DocumentDB";
        telemetry.Name = operationName;
        telemetry.Data = request.RequestUri.OriginalString;

        telemetry.ResultCode = ((int)response.StatusCode).ToString();
        telemetry.Success = response.IsSuccessStatusCode;

        telemetry.Metrics["RequestCharge"] = response.Headers.RequestCharge;

        return response;
    }

    private static readonly Dictionary<string, string> OperationNames = new()
    {
        // Database operations
        ["POST /dbs"] = "Create database",
        ["GET /dbs"] = "List databases",
        ["GET /dbs/*"] = "Get database",
        ["DELETE /dbs/*"] = "Delete database",

        // Collection operations
        ["POST /dbs/*/colls"] = "Create collection",
        ["GET /dbs/*/colls"] = "List collections",
        ["POST /dbs/*/colls/*"] = "Query documents",
        ["GET /dbs/*/colls/*"] = "Get collection",
        ["DELETE /dbs/*/colls/*"] = "Delete collection",
        ["PUT /dbs/*/colls/*"] = "Replace collection",

        // Document operations
        ["POST /dbs/*/colls/*/docs"] = "Create document",
        ["GET /dbs/*/colls/*/docs"] = "List documents",
        ["GET /dbs/*/colls/*/docs/*"] = "Get document",
        ["PUT /dbs/*/colls/*/docs/*"] = "Replace document",
        ["DELETE /dbs/*/colls/*/docs/*"] = "Delete document"
    };

    private static string? GetPropertyNameForResource(string resourceType)
    {
        // ignore high cardinality resources (documents, attachments, etc.)
        return resourceType switch
        {
            "dbs" => "Database",
            "colls" => "Collection",
            _ => null
        };
    }

    private static string GetOperationName(string operation)
    {
        return OperationNames.TryGetValue(operation, out var operationName) ? operationName : operation;
    }
}
```

When you run the application using this implementation, you can check the dependency telemetry for each operation against Cosmos DB in Application Insights.

![More dependency performance in Application Insights](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/570z0f4ea7mi1a07gn12.png)

In this case, you can see the Request Units consumed for the query in Application Insights.

At the same time, the DB name and container name will be sent as custom properties, so you can easily grasp the RU consumption per container.

![More dependency details in Application Insights](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0kzopm24ojlkotqk5y8y.png)

If it is simply the Request Units consumed, it can be understood by transferring the Cosmos DB logs to Log Analytics, but it is not possible to understand how many Cosmos DB queries are executed by which operations in the actual application, so it is best combined with Application Insights.