Blog

Domine a plataforma e otimize seus gastos!

Implementando o OpenTelemetry em Azure Functions

Implementando o OpenTelemetry em Azure Functions
July 16, 2025

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 ou failure).
      • Define o status apropriado (Ok ou Error) para a Activity, de acordo com o parâmetro de falha.

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 do throw — 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!

Podemos te ajudar com uma revisão 100% gratuita do seu ambiente cloud.

Agendar a revisão gratuita do meu ambiente

Share this post

Inscreva-se para novas postagens

Inscreva-se