Skip to content

Diagnostic support for generated code #398

@janbiehl

Description

@janbiehl

Hey folks,

I would like to discuss telemetry support with you. As we are using Open Telemetry for traces and metrics and we would benefit when generated code would have some sort of integration.

Unfortunately, not all database drivers have native integration for this. For npgsql, for example, there is a NuGet for telemetry support. To my knowledge, however, this does not exist for microsoft.data.sqlite or for the mysqlconnector.

Therefore, I have been considering a possible integration into the generated code. I would like to make a suggestion below. If you are interested, I could start working on an implementation.

// Telemetry.cs

public static class TelemetryConstants
{
    public const string SourceName = "{{GeneratedProjectName}}";
}

internal static class Telemetry
{
    private static readonly ActivitySource _activitySource = new (TelemetryConstants.SourceName);

    internal static Activity? StartActivity(string query, Dictionary<string, object?> parameter,
        [CallerMemberName] string name = "")
    {
        if (!_activitySource.HasListener())
            return null; // optimize Szenarios when no one is interested in the information

        List<KeyValuePair<string, object?>> tags =
        [
            new ("db.system", "{{Driver}}"),
            new ("db.statement", query),
        ];

        foreach (var (key, value) in parameter)
        {
            tags.Add(new KeyValuePair<string, object?>($"db.parameter.{key}", value?.ToString() ?? "null"));
        }

        return _activitySource.StartActivity(ActivityKind.Internal, default, tags, null, default, name);
    }
}

And for the generated code we would just need

    public async Task<List<ListItemsPaginatedRow>> ListItemsPaginated(ListItemsPaginatedArgs args)
    {
        var queryParams = new Dictionary<string, object?>();
        queryParams.Add("offset", args.Offset);
        queryParams.Add("limit", args.Limit);

        using var activity = Telemetry.StartActivity(ListItemsPaginatedSql, queryParams);

        try
        {
            if (this.Transaction == null)
            {
                using (var connection = new SqliteConnection(ConnectionString))
                {
                    var result = await connection.QueryAsync<ListItemsPaginatedRow>(ListItemsPaginatedSql, queryParams);
                    return result.AsList();
                }
            }
    
            if (this.Transaction?.Connection == null || this.Transaction?.Connection.State != System.Data.ConnectionState.Open)
                throw new InvalidOperationException("Transaction is provided, but its connection is null.");
            return (await this.Transaction.Connection.QueryAsync<ListItemsPaginatedRow>(ListItemsPaginatedSql, queryParams, transaction: this.Transaction)).AsList();
        }
        catch (Exception e)
        {
            activity?.SetStatus(System.Diagnostics.ActivityStatusCode.Error, e.Message);
            activity?.RecordException(e);
            throw;
        }
    }

This would result in something like below for the observability stack in jaeger. It provides super useful information for optimizing queries by seeing the exact timing. Errors would also be reported and we are able to trace the exact query with all parameters.

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions