Blog
Domine a plataforma e otimize seus gastos!
Implementando o OpenTelemetry em Azure Functions

O OpenTelemetry é um conjunto de especificações, APIs e SDKs de código aberto que permitem a coleta padronizada de métricas, traces e logs em aplicações distribuídas. Neste post, vamos implementar o OpenTelemetry em um projeto de Azure Functions escrito em C# no modelo isolated, garantindo observabilidade completa das nossas functions. Para mais detalhes sobre como as Azure Functions funcionam, confira a documentação oficial.
Vamos começar criando um diretório raiz para o nosso projeto:
mkdir AzureFunctionsOpenTelemetry
cd AzureFunctionsOpenTelemetry
Dentro dele, inicialize uma solution .NET e adicione um arquivo .gitignore.>
dotnet new sln
dotnet new gitignore
Agora inicialize o projeto de Azure Functions usando o runtime isolado do .NET:
func init Functions --worker-runtime dotnet-isolated
Adicione o projeto recém-criado à solution:
dotnet sln add Functions/Functions.csproj
Gere uma function do tipo Timer Trigger chamada MyTimerTrigger
:
func new --template "Timer Trigger" --name MyTimerTrigger
Edite o arquivo Functions/MyTimerTrigger.cs
para ficar assim:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Functions;
public class MyTimerTrigger(ILoggerFactory loggerFactory)
{
private readonly ILogger _logger = loggerFactory.CreateLogger<MyTimerTrigger>();
[Function("MyTimerTrigger")]
public void Run([TimerTrigger("*/10 * * * * *")] TimerInfo myTimer)
{
_logger.LogInformation("C# Timer trigger function executed at: {executionTime}", DateTime.Now);
}
}
Execute func start
para verificar que a trigger dispara a cada 10 segundos.
func start

O nosso projeto de functions está funcionando. Vamos agora implementar o OpenTelemetry!
Para visualizar métricas e traces, vamos criar um dashboard Aspire via Docker Compose. Crie um arquivo docker-compose.yaml
com o seguinte conteúdo:
version: '3.8'
name: mySuperApp.Functions
services:
aspire-dashboard:
image: mcr.microsoft.com/dotnet/aspire-dashboard:9.0
container_name: aspire-dashboard
ports:
- "18888:18888"
- "4317:18889"
environment:
- ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true
restart: unless-stopped
Suba o container:
docker compose up -d
Nos logs do container você verá uma URL como http://localhost:18888/login?t=<token>
. Acesse-a para abrir o dashboard.

http://localhost:18888/login?t=aaf06887b9142d8b7897b84ff4fa009e

Nosso dashboard está fucionando!
De volta ao projeto de Functions, adicione o exporter OTLP:
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
Crie a classe Functions/ConfigurationExtensions.cs
com os seguintes métodos de extensão:
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace Functions;
public static class ConfigurationExtensions
{
public static void AddLogging(this FunctionsApplicationBuilder builder, string appName)
{
builder.Logging.AddOpenTelemetry(options =>
{
options.AddOtlpExporter();
options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(appName));
options.IncludeFormattedMessage = true;
options.ParseStateValues = true;
options.IncludeScopes = true;
});
}
public static void AddConfigurations(
this IServiceCollection services,
IConfiguration configuration,
string appName)
{
var tp = Sdk.CreateTracerProviderBuilder()
.AddSource("Microsoft.Azure.Functions.Worker")
.SetSampler(new AlwaysOnSampler())
.ConfigureResource(configure => configure
.AddService(serviceName: appName))
.AddOtlpExporter()
.Build();
services.AddSingleton(tp);
}
}
Na classe acima, temos:
- Define métodos de extensão para configuração em projetos Azure Functions.
- Permite configurar logging distribuído com OpenTelemetry para a aplicação.
- No método AddLogging:
- Adiciona um exportador OTLP para logs.
- Configura o recurso com o nome da aplicação (appName) para identificação.
- No método AddConfigurations:
- Cria e configura um TracerProvider.
- Usa AlwaysOnSampler para capturar todas os traces.
- Adiciona exportação OTLP para traces.
- Configura o recurso de traces com o nome da aplicação (appName).
- Adiciona o TracerProvider como singleton no container de injeção de dependência.
Com isso, podemos modificar o Program.cs para utilizar as configurações acima:
using Functions;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
string appName = "MySuperApp.Functions";
var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication()
.AddLogging(appName);;
builder.Services
.AddApplicationInsightsTelemetryWorkerService()
.ConfigureFunctionsApplicationInsights()
.AddConfigurations(builder.Configuration, appName);;
builder.Build().Run();
Voltando a function, vamos modifica-lá:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace Functions;
public class MyTimerTrigger(ILoggerFactory loggerFactory) : BaseFunctionActivityManager
{
private readonly ILogger _logger = loggerFactory.CreateLogger<MyTimerTrigger>();
[Function("MyTimerTrigger")]
public void Run([TimerTrigger("*/10 * * * * *")] TimerInfo myTimer, FunctionContext fc)
{
using var activity = StartFunctionActivity(fc, nameof(MyTimerTrigger));
try
{
// Do some work here
using (StartActivity("Activity 1"))
{
_logger.LogInformation("Task 1 ...: {Time}", DateTime.UtcNow);
}
using (StartActivity("Activity 2"))
{
_logger.LogInformation("Task 2 ...: {Time}", DateTime.UtcNow);
// Activity 2 can have nested activities
using (StartActivity("Activity 3"))
{
_logger.LogInformation("Task 3 ...: {Time}", DateTime.UtcNow);
}
}
FinishFunctionActivity(activity);
}
catch (Exception e)
{
FinishFunctionActivity(activity, failure: true);
_logger.LogError(e, "error occurred while executing the timer function");
}
}
}
Alguns detalhes da implementação:
- Azure Function do tipo Timer Trigger que executa a cada 10 segundos.
- Utiliza um logger do ILoggerFactory para gerar logs com contexto.
- Herda de BaseFunctionActivityManager (presumidamente para gerenciar traces e atividades de monitoramento).
- O método Run:
- Inicia uma atividade/trace principal para a função com StartFunctionActivity.
- Executa tarefas em sequência, cada uma envolvida em uma atividade/trace distinta (“Activity 1”, “Activity 2”, “Activity 3”).
- Faz log informativo em cada etapa, incluindo o horário de execução.
- Permite criação de atividades aninhadas para melhor rastreamento (“Activity 3” está dentro de “Activity 2”).
- Finaliza a atividade principal tanto ao final da execução normal (FinishFunctionActivity) quanto em caso de exceção (indicando falha).
- Registra erro em log se algo der errado, detalhando a exceção.
Além disso, temos a classe BaseFunctionActivityManager.cs
using System.Diagnostics;
using Microsoft.Azure.Functions.Worker;
namespace Functions;
public abstract class BaseFunctionActivityManager
{
protected static Activity? StartFunctionActivity(FunctionContext fc, string functionName)
{
var manualInstrumentationSource = Activity.Current?.Source;
if (manualInstrumentationSource == null)
{
return null;
}
var activity = manualInstrumentationSource.StartActivity(functionName);
activity?.AddTag("invocation_id", fc.InvocationId);
activity?.AddTag("created_at", DateTime.UtcNow.ToString("o"));
return activity;
}
protected static Activity? StartActivity(string name)
{
var manualInstrumentationSource = Activity.Current?.Source;
if (manualInstrumentationSource == null)
{
return null;
}
var activity = manualInstrumentationSource.StartActivity(name);
return activity;
}
protected static void FinishFunctionActivity(Activity? activity, bool failure = false)
{
activity?.AddTag("completed_at", DateTime.UtcNow.ToString("o"));
activity?.AddTag("status", failure ? "failure" : "success");
activity?.SetStatus(failure ? ActivityStatusCode.Error : ActivityStatusCode.Ok);
}
}
- Classe abstrata que facilita gerenciamento de atividades (traces) nas Azure Functions.
- Expõe métodos utilitários estáticos para criação, marcação e finalização de “Activities”:
- StartFunctionActivity:
- Inicia uma nova Activity (span) para monitoramento de uma função.
- Usa o nome da função como identificador.
- Adiciona tags ao trace, como o
invocation_id
e o timestamp (created_at
). - Retorna a Activity criada (ou null, se não houver fonte de instrumentação ativa).
- StartActivity:
- Inicia uma Activity genérica com o nome especificado.
- Retorna a Activity criada (ou null).
- FinishFunctionActivity:
- Marca a Activity como finalizada, adicionando tags de conclusão (
completed_at
) e status (success
oufailure
). - Define o status apropriado (Ok ou Error) para a Activity, de acordo com o parâmetro de falha.
- Marca a Activity como finalizada, adicionando tags de conclusão (
- StartFunctionActivity:
Vamos executar novamente as function. Podemos ver os logs gerados no console:

Já no dashboard, podemos ver os tracings enviados pelas functions:

No segundo span, gerado via StartFunctionActivity, podemos ver as tags definidas anteriormente:

Além disso, podemos ver os logs relativos a cada span:


Vamos fazer uma pequena modificação no código:
using (StartActivity("Activity 3"))
{
throw new Exception("Error: This is a test exception in Activity 3");
_logger.LogInformation("Task 3 ...: {Time}", DateTime.UtcNow);
}
- Foi adicionada uma linha que lança uma exceção propositalmente:
throw new Exception("Error: This is a test exception in Activity 3");
- A registro de log (
_logger.LogInformation(...)
) que vinha na sequência agora nunca será executada, pois está abaixo dothrow
— a exceção interrompe imediatamente a execução do bloco. - Com essa alteração, sempre que “Activity 3” for executada, ocorrerá uma exceção, simulando um erro para fins de teste.
Executando novamente o projeto, podemos ver o error fica bem evidente:




Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!
Até a próxima, abraços!