你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:使用 OpenTelemetry 分布式追踪来监控 Azure 函数

本文演示了 Azure Function 中的 OpenTelemetry 支持,它利用集成的 Application Insights 和 OpenTelemetry 支持,实现了跨多个函数调用的分布式跟踪。 为了帮助你入门,Azure 开发人员 CLI (azd) 模板用于创建代码项目以及运行应用的 Azure 部署。

在本教程中,你将使用 azd 该工具来:

  • 从模板中初始化一个已启用 OpenTelemetry 的项目。
  • 请查看支持 OpenTelemetry 集成的代码。
  • 在本地运行并验证已启用 OpenTelemetry 的应用。
  • 在 Azure 中创建函数应用和相关资源。
  • 将代码项目部署到 Azure 中的函数应用。
  • 在 Application Insights 中验证分布式跟踪。

此模板创建所需的 Azure 资源遵循 Azure 中安全且可缩放的函数应用部署的当前最佳做法。 同一 azd 命令还会将代码项目部署到 Azure 中的新函数应用。

默认情况下,Flex Consumption 计划遵循按需付费的计费模型,这意味着使用此计划完成本快速入门会在您的 Azure 帐户中产生少量费用,约为几美元美分或更少。

重要

本文目前仅支持 C#、Python 和 TypeScript。 若要完成快速入门,请在文章顶部选择其中一种受支持的语言。

先决条件

初始化项目

azd init使用命令从包含 OpenTelemetry 分布式跟踪的模板创建本地 Azure Functions 代码项目。

  1. 在本地终端或命令提示符下,在空文件夹中运行此 azd init 命令:

    azd init --template functions-quickstart-python-azd-otel -e flexquickstart-otel
    

    此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。 -e 标志设置当前环境的名称。 在 azd 中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 环境名称也会显示在你在 Azure 中创建的资源组的名称中。

  1. 在本地终端或命令提示符下,在空文件夹中运行此 azd init 命令:

    azd init --template functions-quickstart-typescript-azd-otel -e flexquickstart-otel
    

    此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。 -e 标志设置当前环境的名称。 在 azd 中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 环境名称也会显示在你在 Azure 中创建的资源组的名称中。

  1. 在本地终端或命令提示符下,在空文件夹中运行此 azd init 命令:

    azd init --template functions-quickstart-dotnet-azd-otel -e flexquickstart-otel
    

    此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。 -e 标志设置当前环境的名称。 在 azd 中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 环境名称也会显示在你在 Azure 中创建的资源组的名称中。

查看代码

该模板创建一个完整的分布式跟踪方案,其中包含三个协同工作的函数。 让我们回顾一下与 OpenTelemetry 相关的关键方面:

OpenTelemetry 配置

src/otel-sample/host.json 文件为 Functions 主机启用 OpenTelemetry。

{
  "version": "2.0",
  "telemetryMode": "OpenTelemetry",
  "extensions": {
    "serviceBus": {
        "maxConcurrentCalls": 10
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

密钥设置 "telemetryMode": "OpenTelemetry" 允许跨函数调用进行分布式跟踪。

src/OTelSample/host.json 文件为 Functions 主机启用了 OpenTelemetry:

{
  "version": "2.0",
  "telemetryMode": "OpenTelemetry",
  "logging": {
    "OpenTelemetry": {
      "logLevel": {
        "Host.General": "Warning"
      }
    }
  }
}

密钥设置 "telemetryMode": "OpenTelemetry" 允许跨函数调用进行分布式跟踪。

OpenTelemetry 的依赖项

src/otel-sample/requirements.txt 文件包含 OpenTelemetry 集成所需的包:

azure-functions
azure-monitor-opentelemetry
requests

azure-monitor-opentelemetry 包提供 OpenTelemetry 与 Application Insights 的集成。

src/otel-sample/package.json 文件包含 OpenTelemetry 集成所需的包。

{
  "dependencies": {
    "@azure/functions": "^4.0.0",
    "@azure/functions-opentelemetry-instrumentation": "^0.1.0",
    "@azure/monitor-opentelemetry-exporter": "^1.0.0",
    "axios": "^1.6.0"
  }
}

@azure/functions-opentelemetry-instrumentation@azure/monitor-opentelemetry-exporter 包提供 Application Insights 与 OpenTelemetry 的集成。

.csproj 文件包括 OpenTelemetry 集成所需的包:

<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.4.0" />
<PackageReference Include="Microsoft.Azure.Functions.Worker.OpenTelemetry" Version="1.4.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.10.0" />

这些包提供 OpenTelemetry 与 Application Insights 的集成,以及用于分布式跟踪的 HTTP 监测工具。

函数实现

src/otel-sample/function_app.py 中的函数演示了分布式跟踪的流程。

第一个 HTTP 函数

@app.function_name("first_http_function")
@app.route(route="first_http_function", auth_level=func.AuthLevel.ANONYMOUS)
def first_http_function(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function (first) processed a request.')

    # Call the second function
    base_url = f"{req.url.split('/api/')[0]}/api"
    second_function_url = f"{base_url}/second_http_function"

    response = requests.get(second_function_url)
    second_function_result = response.text

    result = {
        "message": "Hello from the first function!",
        "second_function_response": second_function_result
    }

    return func.HttpResponse(
        json.dumps(result),
        status_code=200,
        mimetype="application/json"
    )

第二个 HTTP 函数

@app.function_name("second_http_function")
@app.route(route="second_http_function", auth_level=func.AuthLevel.ANONYMOUS)
@app.service_bus_queue_output(arg_name="outputsbmsg", queue_name="%ServiceBusQueueName%",
                              connection="ServiceBusConnection")
def second_http_function(req: func.HttpRequest, outputsbmsg: func.Out[str]) -> func.HttpResponse:
    logging.info('Python HTTP trigger function (second) processed a request.')

    message = "This is the second function responding."

    # Send a message to the Service Bus queue
    queue_message = "Message from second HTTP function to trigger ServiceBus queue processing"
    outputsbmsg.set(queue_message)
    logging.info('Sent message to ServiceBus queue: %s', queue_message)

    return func.HttpResponse(
        message,
        status_code=200
    )

服务总线队列触发器

@app.service_bus_queue_trigger(arg_name="azservicebus", queue_name="%ServiceBusQueueName%",
                               connection="ServiceBusConnection") 
def servicebus_queue_trigger(azservicebus: func.ServiceBusMessage):
    logging.info('Python ServiceBus Queue trigger start processing a message: %s',
                azservicebus.get_body().decode('utf-8'))
    time.sleep(5)  # Simulate processing work
    logging.info('Python ServiceBus Queue trigger end processing a message')

OpenTelemetry 配置设置在 src/otel-sample/index.ts 中。

import { AzureFunctionsInstrumentation } from '@azure/functions-opentelemetry-instrumentation';
import { AzureMonitorTraceExporter, AzureMonitorLogExporter } from '@azure/monitor-opentelemetry-exporter';
import { getNodeAutoInstrumentations, getResourceDetectors } from '@opentelemetry/auto-instrumentations-node';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { detectResources } from '@opentelemetry/resources';
import { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { NodeTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';

const resource = detectResources({ detectors: getResourceDetectors() });

const tracerProvider = new NodeTracerProvider({ 
  resource, 
  spanProcessors: [new SimpleSpanProcessor(new AzureMonitorTraceExporter())] 
});
tracerProvider.register();

const loggerProvider = new LoggerProvider({
  resource,
  processors: [new SimpleLogRecordProcessor(new AzureMonitorLogExporter())],
});

registerInstrumentations({
    tracerProvider,
    loggerProvider,
    instrumentations: [getNodeAutoInstrumentations(), new AzureFunctionsInstrumentation()],
});

函数在 src/otel-sample/src/functions 文件夹中定义:

第一个 HTTP 函数

export async function firstHttpFunction(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  context.log("TypeScript HTTP trigger function (first) processed a request.");

  try {
    // Call the second function
    const baseUrl = request.url.split("/api/")[0];
    const secondFunctionUrl = `${baseUrl}/api/second_http_function`;

    const response = await axios.get(secondFunctionUrl);
    const secondFunctionResult = response.data;

    const result = {
      message: "Hello from the first function!",
      second_function_response: secondFunctionResult,
    };

    return {
      status: 200,
      body: JSON.stringify(result),
      headers: { "Content-Type": "application/json" },
    };
  } catch (error) {
    return {
      status: 500,
      body: JSON.stringify({ error: "Failed to process request" }),
    };
  }
}

第二个 HTTP 函数

export async function secondHttpFunction(
  request: HttpRequest,
  context: InvocationContext
): Promise<HttpResponseInit> {
  context.log("TypeScript HTTP trigger function (second) processed a request.");

  const message = "This is the second function responding.";

  // Send a message to the Service Bus queue
  const queueMessage =
    "Message from second HTTP function to trigger ServiceBus queue processing";

  context.extraOutputs.set(serviceBusOutput, queueMessage);
  context.log("Sent message to ServiceBus queue:", queueMessage);

  return {
    status: 200,
    body: message,
  };
}

服务总线队列触发器

export async function serviceBusQueueTrigger(
  message: unknown,
  context: InvocationContext
): Promise<void> {
  context.log("TypeScript ServiceBus Queue trigger start processing a message:", message);

  // Simulate processing time
  await new Promise((resolve) => setTimeout(resolve, 5000));

  context.log("TypeScript ServiceBus Queue trigger end processing a message");
}

OpenTelemetry 配置在 src/OTelSample/Program.cs 中设置。

using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.OpenTelemetry;
using OpenTelemetry.Trace;

var builder = FunctionsApplication.CreateBuilder(args);

builder.ConfigureFunctionsWebApplication();

builder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
});

builder.Services.AddOpenTelemetry()    
    .WithTracing(tracing =>
    {
        tracing.AddHttpClientInstrumentation();
    });

builder.Services.AddOpenTelemetry().UseAzureMonitorExporter();
builder.Services.AddOpenTelemetry().UseFunctionsWorkerDefaults();

builder.Services.AddHttpClient();

builder.Build().Run();

函数在单独的类文件中定义:

第一个 HTTP 函数

public class FirstHttpTrigger
{
    private readonly ILogger<FirstHttpTrigger> _logger;
    private readonly IHttpClientFactory _httpClientFactory;

    public FirstHttpTrigger(ILogger<FirstHttpTrigger> logger, IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;
    }

    [Function("first_http_function")]
    public async Task<IActionResult> Run(
         [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req)
    {
        _logger.LogInformation("first_http_function function processed a request.");

        var baseUrl = $"{req.Url.AbsoluteUri.Split("/api/")[0]}/api";
        var targetUri = $"{baseUrl}/second_http_function";

        var client = _httpClientFactory.CreateClient();
        var response = await client.GetAsync(targetUri);
        var content = await response.Content.ReadAsStringAsync();

        return new OkObjectResult($"Called second_http_function, status: {response.StatusCode}, content: {content}");
    }
}

第二个 HTTP 函数

public class SecondHttpTrigger
{
    private readonly ILogger<SecondHttpTrigger> _logger;

    public SecondHttpTrigger(ILogger<SecondHttpTrigger> logger)
    {
        _logger = logger;
    }

    [Function("second_http_function")]
    public MultiResponse Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req)
    {
        _logger.LogInformation("second_http_function function processed a request.");

        return new MultiResponse
        {
            Messages = new string[] { "Hello" },
            HttpResponse = req.CreateResponse(System.Net.HttpStatusCode.OK)
        };
    }
}

public class MultiResponse
{
    [ServiceBusOutput("%ServiceBusQueueName%", Connection = "ServiceBusConnection")]
    public string[]? Messages { get; set; }

    [HttpResult]
    public HttpResponseData? HttpResponse { get; set; }
}

服务总线队列触发器

public class ServiceBusQueueTrigger
{
    private readonly ILogger<ServiceBusQueueTrigger> _logger;

    public ServiceBusQueueTrigger(ILogger<ServiceBusQueueTrigger> logger)
    {
        _logger = logger;
    }

    [Function("servicebus_queue_trigger")]
    public async Task Run(
        [ServiceBusTrigger("%ServiceBusQueueName%", Connection = "ServiceBusConnection")]
        ServiceBusReceivedMessage message,
        ServiceBusMessageActions messageActions)
    {
        _logger.LogInformation("Message ID: {id}", message.MessageId);
        _logger.LogInformation("Message Body: {body}", message.Body);

        // Complete the message
        await messageActions.CompleteMessageAsync(message);
    }
}

分布式跟踪流

此体系结构创建完整的分布式跟踪方案,并具有以下行为:

  1. 第一个 HTTP 函数 接收 HTTP 请求并调用第二个 HTTP 函数
  2. 第二个 HTTP 函数 响应并向服务总线发送消息
  3. 服务总线触发器 会延迟处理消息,以模拟处理工作

OpenTelemetry 实施的关键方面:

  • OpenTelemetry 集成host.json 文件通过 "telemetryMode": "OpenTelemetry" 启用 OpenTelemetry
  • 函数链接:第一个函数使用 HTTP 请求调用第二个函数,创建相关跟踪
  • 服务总线集成:第二个函数输出到服务总线,这会触发第三个函数
  • 匿名身份验证:HTTP 函数使用 auth_level=func.AuthLevel.ANONYMOUS,因此不需要函数密钥

可在此处查看完整的模板项目。

  • OpenTelemetry 集成index.ts 文件使用 Azure Monitor 导出程序配置 OpenTelemetry,用于跟踪和日志记录
  • 函数链接:第一个函数使用 axios 调用第二个函数,并自动进行跟踪传播
  • 服务总线集成:第二个函数使用输出绑定输出到服务总线,这会触发第三个函数
  • 托管标识:所有服务总线连接都使用托管标识而不是连接字符串
  • 处理模拟:服务总线触发器中的 5 秒延迟模拟消息处理工作

可在此处查看完整的模板项目。

  • OpenTelemetry 集成Program.cs 文件使用 Azure Monitor 导出程序配置 OpenTelemetry
  • 函数链接:第一个函数使用 HttpClient 和 OpenTelemetry 检测调用第二个函数
  • 服务总线集成:第二个函数使用输出绑定输出到服务总线,这会触发第三个函数
  • 托管标识:所有服务总线连接都使用托管标识而不是连接字符串
  • .NET 8 独立辅助角色:使用最新的 Azure Functions .NET 独立辅助角色模型来提高性能和灵活性

可在此处查看完整的模板项目。

在本地验证函数后,可以将其发布到 Azure。

部署到 Azure 云

此项目配置为使用 azd up 命令将此项目部署到 Azure 中具有 OpenTelemetry 支持的 Flex Consumption 计划中的新函数应用。

小窍门

该项目包括一组 Bicep 文件,azd 使用这些文件按照最佳做法为 Flex 消耗计划创建安全部署,包括托管标识连接。

  1. 运行以下命令,让 azd 在 Azure 中创建所需的 Azure 资源,并将代码项目部署到新的函数应用:

    azd up
    

    根文件夹包含 azd 所需的 azure.yaml 定义文件。

    如果尚未登录,系统会要求使用 Azure 帐户进行身份验证。

  2. 出现提示时,请提供以下所需的部署参数:

    参数 Description
    Azure 订阅 要在其中创建资源的订阅。
    Azure 位置 要在其中创建包含新 Azure 资源的资源组的 Azure 区域。 仅显示当前支持 Flex 消耗计划的区域。

    azd up 命令使用你对这些提示的回复和 Bicep 配置文件来完成这些部署任务:

    • 创建和配置这些所需的 Azure 资源(等效于 azd provision):

      • 启用了 OpenTelemetry 的 Azure Functions Flex 消耗计划和函数应用
      • Azure 存储(必需)和 Application Insights(推荐)
      • 用于分布式跟踪演示的服务总线命名空间和队列
      • 帐户的访问策略和角色
      • 使用托管标识(而不是存储的连接字符串)的服务到服务连接
    • 打包代码并将其部署到部署容器(等效于 azd deploy)。 然后,应用将启动并在已部署的包中运行。

    命令成功完成后,你会看到指向所创建资源的链接。

测试分布式跟踪

现在,可以通过调用已部署的函数并观察 Application Insights 中的遥测数据来测试 OpenTelemetry 分布式跟踪功能。

在 Azure 上调用函数

可以通过向 Azure 中的函数终结点发出 HTTP 请求来调用其 URL。 由于此模板中的 HTTP 函数配置了匿名访问,因此不需要任何函数密钥。

  1. 在本地终端或命令提示符中,运行以下命令以获取函数应用名称并构造 URL:

    APP_NAME=$(azd env get-value AZURE_FUNCTION_NAME)
    echo "Function URL: https://$APP_NAME.azurewebsites.net/api/first_http_function"
    

    azd env get-value 命令从本地环境中获取函数应用名称。

  2. 通过导航到 URL 在浏览器中测试函数:

    https://your-function-app.azurewebsites.net/api/first_http_function
    

    your-function-app替换为上一步中的实际函数应用名称。 此单个请求创建一个分布式跟踪,该跟踪流经所有三个函数。

在 Application Insights 中查看分布式跟踪

调用函数后,可以在 Application Insights 中观察完整的分布式跟踪:

注释

调用函数后,遥测数据可能需要几分钟才会显示在 Application Insights 中。 如果未立即看到数据,请等待几分钟并刷新视图。

  1. 在 Azure 门户中访问您的 Application Insights 资源(可以在与您的函数应用相同的资源组中找到)。

  2. 打开 应用程序映射 ,查看所有三个函数的分布式跟踪。 您应该能够看到从 HTTP 请求,通过您的函数,到达服务总线的流程。

  3. 检查 事务搜索 以查找请求,并查看完整的跟踪时间线。 在函数应用中搜索事务。

  4. 选择一个特定事务以查看其显示的端到端跟踪:

    • first_http_function进行HTTP请求
    • 对内部 HTTP 调用 second_http_function
    • 正在发送的服务总线消息
    • servicebus_queue_trigger 正在处理来自服务总线的消息
  5. 在跟踪详细信息中,可以看到:

    • 计时信息:每个步骤花费的时间
    • 依赖项:函数之间的连接
    • 日志:与跟踪关联的应用程序日志
    • 性能指标:响应时间和吞吐量

此示例演示了使用 OpenTelemetry 集成跨多个 Azure Functions 的端到端分布式跟踪,从而完全了解应用程序的行为和性能。

重新部署代码

根据需要多次运行 azd up 命令,以预配 Azure 资源并为函数应用部署代码更新。

注释

最新的部署包始终会覆盖已部署的代码文件。

azd 提示的初始响应和 azd 生成的任何环境变量都本地存储在你的命名环境中。 azd env get-values使用命令查看创建 Azure 资源时该命令使用的环境中的所有变量。

清理资源

使用完函数应用和相关资源后,请使用此命令从 Azure 中删除函数应用及其相关资源,并避免产生任何进一步的成本:

azd down --no-prompt

注释

--no-prompt 选项指示 azd 在未经你确认的情况下删除资源组。

此命令不会影响本地代码项目。