Skip to content

Instantly share code, notes, and snippets.

@mrpmorris
Last active January 21, 2024 18:26
Show Gist options
  • Save mrpmorris/533871de563acd217207e772ade707d7 to your computer and use it in GitHub Desktop.
Save mrpmorris/533871de563acd217207e772ade707d7 to your computer and use it in GitHub Desktop.
Azure function telemetry
public class ActivityTrackingMiddleware : IFunctionsWorkerMiddleware
{
public const string ActivitySourceName = "AzureFunctionsWorker";
private readonly static ConcurrentDictionary<string, TriggerParameterInfo> FunctionIdToTriggerParameterInfoLookup = new();
private readonly static ActivitySource ActivitySource = new(ActivitySourceName);
private readonly HttpTriggerHandler HttpTriggerHandler;
private readonly ActivityTriggerHandler ActivityTriggerHandler;
private readonly FrozenDictionary<string, ICustomTriggerHandler> CustomTriggerHandlers;
public ActivityTrackingMiddleware(
HttpTriggerHandler httpTriggerHandler,
ActivityTriggerHandler activityTriggerHandler,
IEnumerable<ICustomTriggerHandler> customTriggerHandlers)
{
HttpTriggerHandler = httpTriggerHandler ?? throw new ArgumentNullException(nameof(httpTriggerHandler));
CustomTriggerHandlers = customTriggerHandlers.ToFrozenDictionary(x => x.GetTriggerTypeName());
ActivityTriggerHandler = activityTriggerHandler ?? throw new ArgumentNullException(nameof(activityTriggerHandler));
}
public async Task Invoke(FunctionContext context, FunctionExecutionDelegate next)
{
Activity? activity = null;
TriggerParameterInfo triggerParameterInfo = GetTriggerParameterInfoData(context.FunctionDefinition);
try
{
string triggerType = triggerParameterInfo.BindingMetadata.Type;
ITriggerHandler? handler = triggerType switch
{
"httpTrigger" => HttpTriggerHandler,
//"activityTrigger" => ActivityTriggerHandler,
_ => CustomTriggerHandlers.TryGetValue(triggerType, out var h) ? h : null,
};
activity =
handler is null
? await PassthroughHandlerAsync(context, next)
: await handler.HandleAsync(
ActivitySource,
triggerParameterInfo,
context,
next);
if (activity is not null)
{
activity.SetTag(TraceSemanticConventions.AttributeFaasInvokedName, context.FunctionDefinition.Name);
activity.SetTag(TraceSemanticConventions.AttributeFaasExecution, context.InvocationId);
activity.SetTag(FunctionActivityConstants.Entrypoint, context.FunctionDefinition.EntryPoint);
activity.SetTag(FunctionActivityConstants.Id, context.FunctionDefinition.Id);
}
}
finally
{
activity?.Dispose();
}
}
private async Task<Activity?> PassthroughHandlerAsync(FunctionContext context, FunctionExecutionDelegate next)
{
Activity? result = ActivitySource.StartActivity("Function Executed", ActivityKind.Server);
await next(context);
return result;
}
private static TriggerParameterInfo GetTriggerParameterInfoData(FunctionDefinition functionDefinition) =>
FunctionIdToTriggerParameterInfoLookup
.GetOrAdd(
key: functionDefinition.Id,
valueFactory: _ => GetTriggerParameterInfo(functionDefinition));
private static TriggerParameterInfo GetTriggerParameterInfo(FunctionDefinition functionDefinition)
{
foreach (FunctionParameter parameter in functionDefinition.Parameters)
{
foreach (KeyValuePair<string, object> kvp in parameter.Properties)
{
if (kvp.Value is TriggerBindingAttribute attribute)
return new TriggerParameterInfo(
parameter,
functionDefinition.InputBindings[parameter.Name],
attribute);
}
}
throw new InvalidOperationException(
$"Function \"{functionDefinition.Name}\" does not have a parameter"
+ $" decorated with a \"{nameof(TriggerBindingAttribute)}\".");
}
}
public class AzureResourceDetector : IResourceDetector
{
public Resource Detect()
{
var resource = ResourceBuilder.CreateEmpty();
var envVars = Environment.GetEnvironmentVariables();
var attributesToAdd = new List<KeyValuePair<string, object>>();
var envVarsToAdd = new List<Tuple<string, string>> {
new("azure.appservice.site_name", "WEBSITE_SITE_NAME"),
new("azure.resource_group", "WEBSITE_RESOURCE_GROUP"),
new("azure.subscription_id", "WEBSITE_OWNER_NAME"),
new("azure.region", "REGION_NAME"),
new("azure.appservice.platform_version", "WEBSITE_PLATFORM_VERSION"),
new("azure.appservice.sku", "WEBSITE_SKU"),
new("azure.appservice.bitness", "SITE_BITNESS"), // x86 vs AMD64
new("azure.appservice.hostname", "WEBSITE_HOSTNAME"),
new("azure.appservice.role_instance_id", "WEBSITE_ROLE_INSTANCE_ID"),
new("azure.appservice.slot_name", "WEBSITE_SLOT_NAME"),
new("azure.appservice.instance_id", "WEBSITE_INSTANCE_ID"),
new("azure.appservice.website_logging_enabled", "WEBSITE_HTTPLOGGING_ENABLED"),
new("azure.appservice.internal_ip", "WEBSITE_PRIVATE_IP"),
new("azure.appservice.functions_extensions_version", "FUNCTIONS_EXTENSION_VERSION"),
new("azure.appservice.functions.worker_runtime", "FUNCTIONS_WORKER_RUNTIME"),
new("azure.appservice.function_placeholder_mode", "WEBSITE_PLACEHOLDER_MODE"),
};
resource.AddAttributes(
envVarsToAdd
.Where(attr => envVars.Contains(attr.Item2) &&
!string.IsNullOrEmpty(envVars[attr.Item2]?.ToString()))
.Select(attr =>
{
var (name, key) = attr;
return new KeyValuePair<string, object>(name, envVars[key]?.ToString()!);
})
);
resource.AddAttributes(attributesToAdd);
return resource.Build();
}
}
internal static class FunctionActivityConstants
{
public const string Entrypoint = "azure.function.entrypoint";
public const string Id = "azure.function.id";
}
public class HttpTriggerHandler : ITriggerHandler
{
public async Task<Activity?> HandleAsync(
ActivitySource activitySource,
TriggerParameterInfo triggerParameterInfo,
FunctionContext context,
FunctionExecutionDelegate next)
{
HttpRequestData? requestData = await context.GetHttpRequestDataAsync();
if (requestData is null)
return null;
ActivityContext currentActivityContext = Activity.Current?.Context ?? new ActivityContext();
var propagationContext = new PropagationContext(currentActivityContext, Baggage.Current);
PropagationContext newActivityContext =
Propagators
.DefaultTextMapPropagator
.Extract(
context: propagationContext,
carrier: requestData.Headers,
getter: ExtractContextFromHeaderCollection);
string? route = ((HttpTriggerAttribute)triggerParameterInfo.BindingAttribute).Route ?? requestData.Url.AbsolutePath;
string activityName = $"{requestData.Method.ToUpper()} {context.FunctionDefinition.Name}";
Activity? activity = activitySource
.StartActivity(
activityName,
ActivityKind.Server,
newActivityContext.ActivityContext);
activity?.SetTag(TraceSemanticConventions.AttributeHttpRoute, route);
activity?.SetTag(TraceSemanticConventions.AttributeHttpMethod, requestData.Method);
activity?.SetTag(TraceSemanticConventions.AttributeHttpTarget, requestData.Url);
activity?.SetTag(TraceSemanticConventions.AttributeNetHostName, requestData.Url.Host);
activity?.SetTag(TraceSemanticConventions.AttributeNetHostPort, requestData.Url.Port);
activity?.SetTag(TraceSemanticConventions.AttributeHttpScheme, requestData.Url.Scheme);
activity?.SetTag(TraceSemanticConventions.AttributeHttpRequestContentLength, requestData.Body.Length);
try
{
await next(context);
}
finally
{
if (context.GetHttpResponseData() is { } responseData)
{
activity?.SetTag(TraceSemanticConventions.AttributeHttpStatusCode, responseData.StatusCode);
activity?.SetTag(TraceSemanticConventions.AttributeHttpResponseContentLength, responseData.Body.Length);
}
}
return activity;
}
internal static IEnumerable<string> ExtractContextFromHeaderCollection(HttpHeadersCollection headersCollection, string key) =>
headersCollection.TryGetValues(key, out var propertyValue) ? propertyValue : [];
}
public interface ICustomTriggerHandler : ITriggerHandler
{
string GetTriggerTypeName();
}
public interface ITriggerHandler
{
Task<Activity?> HandleAsync(
ActivitySource activitySource,
TriggerParameterInfo triggerParameterInfo,
FunctionContext context,
FunctionExecutionDelegate next);
}
public static class OpenTelemetryFunctionWorkerExtensions
{
public static IFunctionsWorkerApplicationBuilder AddOpenTelemetry(this IFunctionsWorkerApplicationBuilder builder)
{
builder.UseMiddleware<ActivityTrackingMiddleware>();
builder.Services.TryAddSingleton<AzureResourceDetector>();
RegisterHandlersForWellKnownTriggers(builder);
builder.Services.ConfigureOpenTelemetryTracerProvider((serviceProvider, tracerProvider) =>
tracerProvider
.ConfigureResource(resourceBuilder => resourceBuilder
.AddDetector(serviceProvider.GetRequiredService<AzureResourceDetector>())
)
.AddSource(ActivityTrackingMiddleware.ActivitySourceName)
);
return builder;
}
private static void RegisterHandlersForWellKnownTriggers(IFunctionsWorkerApplicationBuilder builder)
{
builder.Services.TryAddSingleton<HttpTriggerHandler>();
builder.Services.TryAddSingleton<OrchestrationTriggerHandler>();
}
}
public class OrchestrationTriggerHandler : ITriggerHandler
{
public Task<Activity?> HandleAsync(
ActivitySource activitySource,
BindingMetadata bindingMetaData,
FunctionContext context,
FunctionExecutionDelegate next)
{
throw new NotImplementedException();
}
}
public readonly record struct TriggerParameterInfo(
FunctionParameter Parameter,
BindingMetadata BindingMetadata,
TriggerBindingAttribute BindingAttribute)
{
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment