DEV Community

Cover image for Collecting Cosmos DB dependency telemetry with Application Insights
Tatsuro Shibamura
Tatsuro Shibamura

Posted on

Collecting Cosmos DB dependency telemetry with Application Insights

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 logo Azure / azure-cosmos-dotnet-v3

.NET SDK for Azure Cosmos DB for the core SQL API

NuGet NuGet Prerelease

Microsoft Azure Cosmos DB .NET SDK Version 3

This client library enables client applications to connect to Azure Cosmos via the SQL API. Azure Cosmos is a globally distributed, multi-model database service. For more information, refer to https://azure.microsoft.com/services/cosmos-db/.

CosmosClient client = new CosmosClient("https://mycosmosaccount.documents.azure.com:443/", "mysupersecretkey")
Database database = await client.CreateDatabaseIfNotExistsAsync("MyDatabaseName");
Container container = await database.CreateContainerIfNotExistsAsync(
    "MyContainerName",
    "/partitionKeyPath",
    400);

// Create an item
dynamic testItem = new { id = "MyTestItemId", partitionKeyPath = "MyTestPkValue", details = "it's working", status = "done" };
ItemResponse<dynamic> createResponse = await container.CreateItemAsync(testItem);

// Query for an item
using (FeedIterator<dynamic> feedIterator = await container.GetItemQueryIterator<dynamic>(
โ€ฆ
Enter fullscreen mode Exit fullscreen mode

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.

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.

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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 }
    });
});
Enter fullscreen mode Exit fullscreen mode

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

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

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.

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.

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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.

Discussion (0)