Skip to content

Instantly share code, notes, and snippets.

@benmccallum
Last active October 2, 2024 17:40
Show Gist options
  • Save benmccallum/8768fe09fe185f78dbe4c36d59c4c88b to your computer and use it in GitHub Desktop.
Save benmccallum/8768fe09fe185f78dbe4c36d59c4c88b to your computer and use it in GitHub Desktop.
HotChocolate logging example

Example of a diagnostic event listener in HC that logs to Microsoft.Extensions.ILogger and also to NewRelic.

You need to register it:

IRequestExecutorBuilder builder;
builder
    .AddDiagnosticEventListener<OurDiagnosticEventListener>();

If you're doing stitching, I believe you need to create this with the service provider overload of AddDiagnosticEventListener like below.

IRequestExecutorBuilder builder;
builder
    .AddDiagnosticEventListener(sp =>
        new OurDiagnosticEventListener(
            shouldLogToNewRelic: !env.IsDevelopment() && !env.IsIntegrationTesting(),
            sp.GetApplicationService<ILogger<OurDiagnosticEventListener>>()))
using System;
using System.Collections.Generic;
using HotChocolate;
using HotChocolate.Execution;
using HotChocolate.Execution.Instrumentation;
using HotChocolate.Resolvers;
using Microsoft.Extensions.Logging;
namespace MyCompany.GraphQL.Execution.Instrumentation
{
/// <summary>
/// A listener for events in HotChocolate.
/// </summary>
public class OurDiagnosticEventListener : DiagnosticEventListener
{
private readonly bool _shouldLogToNewRelic;
private readonly ILogger<OurDiagnosticEventListener> _logger;
public OurDiagnosticEventListener(
bool shouldLogToNewRelic,
ILogger<OurDiagnosticEventListener> logger)
{
_shouldLogToNewRelic = shouldLogToNewRelic;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public override bool EnableResolveFieldValue => true;
public override IActivityScope ExecuteRequest(IRequestContext context)
{
// Add basic context to logger that will be recorded with every log entry
var basicScopedContextData = GetBasicScopeContextData(context);
using var scope = _logger.BeginScope(basicScopedContextData);
if (_shouldLogToNewRelic)
{
var currentTransaction = NewRelic.Api.Agent.NewRelic.GetAgent().CurrentTransaction;
foreach (var kvp in basicScopedContextData)
{
currentTransaction.AddCustomAttribute(kvp.Key, kvp.Value);
}
}
// Log the full query when on Information level
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation(
"Query: {query} QueryId: {queryId} Operation: {operationName}",
context.Request.Query?.ToString() ?? "null",
GetQueryId(context) ?? "null",
context.Request.OperationName ?? "null");
}
var result = base.ExecuteRequest(context);
//context.Result will now have a value
return result;
}
public override void TaskError(IExecutionTask task, IError error)
=> LogError(error);
public override void RequestError(IRequestContext context, Exception exception)
{
_logger.LogError(exception, "Request error");
NewRelic.Api.Agent.NewRelic.NoticeError(exception, GetDetailedScopeContextData(context));
}
public override void SyntaxError(IRequestContext context, IError error)
=> LogError(error, GetDetailedScopeContextData(context));
public override void ValidationErrors(IRequestContext context, IReadOnlyList<IError> errors)
{
foreach (var error in errors)
{
LogError(error, GetDetailedScopeContextData(context));
}
}
public override void ResolverError(IMiddlewareContext context, IError error)
=> LogError(error, GetDetailedScopeContextData(context));
private void LogError(IError error, Dictionary<string, object>? additionalContextData = null)
{
additionalContextData ??= new Dictionary<string, object>();
additionalContextData.Add("errorPath", error.Path?.Print() ?? "unknown");
using var scope = _logger.BeginScope(additionalContextData);
if (error.Exception == null)
{
_logger.LogError(error.Message);
if (_shouldLogToNewRelic)
{
NewRelic.Api.Agent.NewRelic.NoticeError(error.Message, additionalContextData);
}
}
else
{
_logger.LogError(error.Exception, error.Message);
if (_shouldLogToNewRelic)
{
NewRelic.Api.Agent.NewRelic.NoticeError(error.Exception, additionalContextData);
}
}
}
override
private static Dictionary<string, object> GetBasicScopeContextData(IRequestContext requestContext)
{
var data = new Dictionary<string, object>
{
{ "schemaName", requestContext.Schema.Name.Value },
{ "queryId", GetQueryId(requestContext) ?? "null" },
{ "operationName", requestContext.Operation?.Name?.Value ?? "unnamed" },
};
if (requestContext.Request.VariableValues != null)
{
foreach (var (key, value) in requestContext.Request.VariableValues)
{
data.Add($"variable[{key}]", value?.ToString() ?? "null");
}
}
return data;
}
private static Dictionary<string, object> GetDetailedScopeContextData(IRequestContext requestContext)
{
return new Dictionary<string, object>
{
{ "query", requestContext.Request.Query?.ToString() ?? "null" },
};
}
private static Dictionary<string, object> GetDetailedScopeContextData(IMiddlewareContext middlewareContext)
{
return new Dictionary<string, object>
{
{ "document", middlewareContext.Document.ToString(indented: true) },
};
}
private static string? GetQueryId(IRequestContext requestContext)
{
return
requestContext.Request.QueryId ??
requestContext.DocumentId ??
requestContext.DocumentHash ??
requestContext.Request.QueryHash;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment