Last active
January 21, 2024 18:26
-
-
Save mrpmorris/533871de563acd217207e772ade707d7 to your computer and use it in GitHub Desktop.
Azure function telemetry
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)}\"."); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
internal static class FunctionActivityConstants | |
{ | |
public const string Entrypoint = "azure.function.entrypoint"; | |
public const string Id = "azure.function.id"; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 : []; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface ICustomTriggerHandler : ITriggerHandler | |
{ | |
string GetTriggerTypeName(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface ITriggerHandler | |
{ | |
Task<Activity?> HandleAsync( | |
ActivitySource activitySource, | |
TriggerParameterInfo triggerParameterInfo, | |
FunctionContext context, | |
FunctionExecutionDelegate next); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class OrchestrationTriggerHandler : ITriggerHandler | |
{ | |
public Task<Activity?> HandleAsync( | |
ActivitySource activitySource, | |
BindingMetadata bindingMetaData, | |
FunctionContext context, | |
FunctionExecutionDelegate next) | |
{ | |
throw new NotImplementedException(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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