你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
本文演示了 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。 若要完成快速入门,请在文章顶部选择其中一种受支持的语言。
先决条件
拥有有效订阅的 Azure 帐户。 免费创建帐户。
初始化项目
azd init使用命令从包含 OpenTelemetry 分布式跟踪的模板创建本地 Azure Functions 代码项目。
在本地终端或命令提示符下,在空文件夹中运行此
azd init命令:azd init --template functions-quickstart-python-azd-otel -e flexquickstart-otel此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。
-e标志设置当前环境的名称。 在azd中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 环境名称也会显示在你在 Azure 中创建的资源组的名称中。
在本地终端或命令提示符下,在空文件夹中运行此
azd init命令:azd init --template functions-quickstart-typescript-azd-otel -e flexquickstart-otel此命令从模板存储库中拉取项目文件,并在当前文件夹中初始化项目。
-e标志设置当前环境的名称。 在azd中,环境可维护应用的唯一部署上下文,你可以定义多个环境。 环境名称也会显示在你在 Azure 中创建的资源组的名称中。
在本地终端或命令提示符下,在空文件夹中运行此
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);
}
}
分布式跟踪流
此体系结构创建完整的分布式跟踪方案,并具有以下行为:
- 第一个 HTTP 函数 接收 HTTP 请求并调用第二个 HTTP 函数
- 第二个 HTTP 函数 响应并向服务总线发送消息
- 服务总线触发器 会延迟处理消息,以模拟处理工作
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 消耗计划创建安全部署,包括托管标识连接。
运行以下命令,让
azd在 Azure 中创建所需的 Azure 资源,并将代码项目部署到新的函数应用:azd up根文件夹包含
azd所需的azure.yaml定义文件。如果尚未登录,系统会要求使用 Azure 帐户进行身份验证。
出现提示时,请提供以下所需的部署参数:
参数 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 函数配置了匿名访问,因此不需要任何函数密钥。
在本地终端或命令提示符中,运行以下命令以获取函数应用名称并构造 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命令从本地环境中获取函数应用名称。通过导航到 URL 在浏览器中测试函数:
https://your-function-app.azurewebsites.net/api/first_http_function将
your-function-app替换为上一步中的实际函数应用名称。 此单个请求创建一个分布式跟踪,该跟踪流经所有三个函数。
在 Application Insights 中查看分布式跟踪
调用函数后,可以在 Application Insights 中观察完整的分布式跟踪:
注释
调用函数后,遥测数据可能需要几分钟才会显示在 Application Insights 中。 如果未立即看到数据,请等待几分钟并刷新视图。
在 Azure 门户中访问您的 Application Insights 资源(可以在与您的函数应用相同的资源组中找到)。
打开 应用程序映射 ,查看所有三个函数的分布式跟踪。 您应该能够看到从 HTTP 请求,通过您的函数,到达服务总线的流程。
检查 事务搜索 以查找请求,并查看完整的跟踪时间线。 在函数应用中搜索事务。
选择一个特定事务以查看其显示的端到端跟踪:
- 向
first_http_function进行HTTP请求 - 对内部 HTTP 调用
second_http_function - 正在发送的服务总线消息
-
servicebus_queue_trigger正在处理来自服务总线的消息
- 向
在跟踪详细信息中,可以看到:
- 计时信息:每个步骤花费的时间
- 依赖项:函数之间的连接
- 日志:与跟踪关联的应用程序日志
- 性能指标:响应时间和吞吐量
此示例演示了使用 OpenTelemetry 集成跨多个 Azure Functions 的端到端分布式跟踪,从而完全了解应用程序的行为和性能。
重新部署代码
根据需要多次运行 azd up 命令,以预配 Azure 资源并为函数应用部署代码更新。
注释
最新的部署包始终会覆盖已部署的代码文件。
对 azd 提示的初始响应和 azd 生成的任何环境变量都本地存储在你的命名环境中。
azd env get-values使用命令查看创建 Azure 资源时该命令使用的环境中的所有变量。
清理资源
使用完函数应用和相关资源后,请使用此命令从 Azure 中删除函数应用及其相关资源,并避免产生任何进一步的成本:
azd down --no-prompt
注释
--no-prompt 选项指示 azd 在未经你确认的情况下删除资源组。
此命令不会影响本地代码项目。