使用 Application Insights .NET SDK 跟踪自定义操作

Application Insights SDK 自动跟踪传入的 HTTP 请求和对依赖服务的调用,例如 HTTP 请求和 SQL 查询。 通过跟踪和关联请求及依赖项,可了解由微服务组成的应用程序对这些微服务的整体响应能力和可靠性。

在一般情况下,有一类应用程序模式不受支持。 正确监视此类模式需要手动代码检测。 本文介绍几种可能需要手动检测的模式,比如自定义队列处理和运行长时间运行的后台任务。

本文提供有关如何使用 Application Insights SDK 跟踪自定义操作的指导。 本文档面向:

  • 适用于 .NET 的 Application Insights(也称为 Base SDK)版本 2.4+。
  • 适用于 Web 应用程序的 Application Insights(运行 ASP.NET)版本 2.4+。
  • 适用于 ASP.NET Core 的 Application Insights 版本 2.1+。

注意

以下文档依赖于 Application Insights 经典 API。 Application Insights 的长期计划是使用 OpenTelemetry 收集数据。 有关详细信息,请参阅为 .NET、Node.js、Python 和 Java 应用程序启用 Azure Monitor OpenTelemetry

概述

操作是由应用程序运行的一项逻辑工作。 它具有名称、开始时间、持续时间、结果和执行上下文(如用户名、属性和结果)。 如果操作 A 由操作 B 启动,则操作 B 设置为 A 的父级。一个操作只能有一个父操作,但可以有许多子操作。 有关操作和遥测关联的详细信息,请参阅 Application Insights 遥测关联

在 Application Insights.NET SDK 中,操作由抽象类 OperationTelemetry 及其后代 RequestTelemetryDependencyTelemetry 描述。

传入操作跟踪

Application Insights Web SDK 自动收集 ASP.NET 应用程序(在 IIS 管道中运行)和所有 ASP.NET Core 应用程序的 HTTP 请求。 有其他平台和框架的社区支持解决方案。 如果应用程序不受任何标准或社区支持解决方案的支持,可以手动进行检测。

需要自定义跟踪的另一个示例是,从队列接收项目的辅助角色。 对于某些队列,向队列添加消息的调用将作为依赖项进行跟踪。 不会自动收集描述消息处理的高级操作。

我们来了解可以跟踪此类操作的方式。

大致而言,此任务旨在创建 RequestTelemetry 并设置已知的属性。 在操作完成后,可跟踪遥测数据。 以下示例演示了此任务。

Owin 自托管应用中的 HTTP 请求

在此示例中,跟踪上下文根据 HTTP 关联协议进行传播。 用户应该会收到此处所述的标头。

public class ApplicationInsightsMiddleware : OwinMiddleware
{
    // You may create a new TelemetryConfiguration instance, reuse one you already have,
    // or fetch the instance created by Application Insights SDK.
    private readonly TelemetryConfiguration telemetryConfiguration = TelemetryConfiguration.CreateDefault();
    private readonly TelemetryClient telemetryClient = new TelemetryClient(telemetryConfiguration);
    
    public ApplicationInsightsMiddleware(OwinMiddleware next) : base(next) {}

    public override async Task Invoke(IOwinContext context)
    {
        // Let's create and start RequestTelemetry.
        var requestTelemetry = new RequestTelemetry
        {
            Name = $"{context.Request.Method} {context.Request.Uri.GetLeftPart(UriPartial.Path)}"
        };

        // If there is a Request-Id received from the upstream service, set the telemetry context accordingly.
        if (context.Request.Headers.ContainsKey("Request-Id"))
        {
            var requestId = context.Request.Headers.Get("Request-Id");
            // Get the operation ID from the Request-Id (if you follow the HTTP Protocol for Correlation).
            requestTelemetry.Context.Operation.Id = GetOperationId(requestId);
            requestTelemetry.Context.Operation.ParentId = requestId;
        }

        // StartOperation is a helper method that allows correlation of 
        // current operations with nested operations/telemetry
        // and initializes start time and duration on telemetry items.
        var operation = telemetryClient.StartOperation(requestTelemetry);

        // Process the request.
        try
        {
            await Next.Invoke(context);
        }
        catch (Exception e)
        {
            requestTelemetry.Success = false;
            requestTelemetry.ResponseCode;
            telemetryClient.TrackException(e);
            throw;
        }
        finally
        {
            // Update status code and success as appropriate.
            if (context.Response != null)
            {
                requestTelemetry.ResponseCode = context.Response.StatusCode.ToString();
                requestTelemetry.Success = context.Response.StatusCode >= 200 && context.Response.StatusCode <= 299;
            }
            else
            {
                requestTelemetry.Success = false;
            }

            // Now it's time to stop the operation (and track telemetry).
            telemetryClient.StopOperation(operation);
        }
    }
    
    public static string GetOperationId(string id)
    {
        // Returns the root ID from the '|' to the first '.' if any.
        int rootEnd = id.IndexOf('.');
        if (rootEnd < 0)
            rootEnd = id.Length;

        int rootStart = id[0] == '|' ? 1 : 0;
        return id.Substring(rootStart, rootEnd - rootStart);
    }
}

HTTP 关联协议还声明 Correlation-Context 标头。 为了简单起见,此处省略了该操作。

队列检测

W3C 跟踪上下文HTTP 关联协议使用 HTTP 请求传递关联详细信息,但每个队列协议必须定义如何随队列消息传递相同的详细信息。 有些队列协议(例如 AMQP)允许传递更多元数据。 其他协议(例如 Azure 存储队列)要求将上下文编码到消息有效负载中。

注意

队列尚不支持跨组件跟踪。

使用 HTTP 时,如果生成者和使用者将遥测数据发送到不同的 Application Insights 资源,则事务诊断体验和应用程序映射将显示事务和端到端映射。 对于队列,尚不支持此功能。

服务总线队列

有关跟踪信息,请参阅通过 Azure 服务总线消息传递进行分布式跟踪和关联

Azure 存储队列

以下示例显示如何跟踪 Azure 存储队列操作,并将生成者、使用者和 Azure 存储之间的遥测相关联。

存储队列具有一个 HTTP API。 用于 HTTP 请求的 Application Insights Dependency Collector 会跟踪对该队列的所有调用。 在 ASP.NET 和 ASP.NET Core 应用程序上默认会配置此功能。 对于其他类型的应用程序,请参阅控制台应用程序文档

用户可能还想将 Application Insights 操作 ID 与存储请求 ID 相关联。 有关如何设置与获取存储请求客户端和服务器请求 ID 的信息,请参阅对 Azure 存储进行监视、诊断和故障排除

排队

由于存储队列支持 HTTP API,因此 Application Insights 会自动跟踪队列的所有操作。 在多数情况下,此检测已足够。 为了将使用者跟踪与生成者跟踪相关联,必须传递某些关联上下文,方法类似于 HTTP 关联协议中所执行的操作。

此示例演示如何跟踪 Enqueue 操作。 方法:

  • 关联重试(如果有) :它们都有一个共同的父级,即 Enqueue 操作。 否则,它们都作为传入请求的子级进行跟踪。 如果有多个对队列的逻辑请求,可能很难发现导致重试的调用。
  • 关联存储日志(如果需要) :它们与 Application Insights 遥测相关联。

Enqueue 操作是父操作的子级。 例如传入的 HTTP 请求。 HTTP 依赖项调用是 Enqueue 操作的子级以及传入请求的孙级。

public async Task Enqueue(CloudQueue queue, string message)
{
    var operation = telemetryClient.StartOperation<DependencyTelemetry>("enqueue " + queue.Name);
    operation.Telemetry.Type = "Azure queue";
    operation.Telemetry.Data = "Enqueue " + queue.Name;

    // MessagePayload represents your custom message and also serializes correlation identifiers into payload.
    // For example, if you choose to pass payload serialized to JSON, it might look like
    // {'RootId' : 'some-id', 'ParentId' : '|some-id.1.2.3.', 'message' : 'your message to process'}
    var jsonPayload = JsonConvert.SerializeObject(new MessagePayload
    {
        RootId = operation.Telemetry.Context.Operation.Id,
        ParentId = operation.Telemetry.Id,
        Payload = message
    });
    
    CloudQueueMessage queueMessage = new CloudQueueMessage(jsonPayload);

    // Add operation.Telemetry.Id to the OperationContext to correlate Storage logs and Application Insights telemetry.
    OperationContext context = new OperationContext { ClientRequestID = operation.Telemetry.Id};

    try
    {
        await queue.AddMessageAsync(queueMessage, null, null, new QueueRequestOptions(), context);
    }
    catch (StorageException e)
    {
        operation.Telemetry.Properties.Add("AzureServiceRequestID", e.RequestInformation.ServiceRequestID);
        operation.Telemetry.Success = false;
        operation.Telemetry.ResultCode = e.RequestInformation.HttpStatusCode.ToString();
        telemetryClient.TrackException(e);
    }
    finally
    {
        // Update status code and success as appropriate.
        telemetryClient.StopOperation(operation);
    }
}  

若要减少应用程序报告的遥测数或者由于其他原因不想跟踪 Enqueue 操作,可直接使用 Activity API:

  • 创建(并启动)新的 Activity,而不是启动 Application Insights 操作。 需在其上分配除操作名称以外的任何属性。
  • yourActivity.Id 串行化到消息有效负载,而不是 operation.Telemetry.Id。 还可以使用 Activity.Current.Id

取消排队

Enqueue 类似,Application Insights 自动跟踪对存储队列的实际 HTTP 请求。 Enqueue 操作可能发生在父上下文中,例如传入请求上下文。 Application Insights SDK 自动将此类操作及其 HTTP 部分与父请求和同一范围内报告的其他遥测相关联。

Dequeue 操作比较棘手。 Application Insights SDK 自动跟踪 HTTP 请求。 但是,在分析消息之前,它并不知道关联上下文。 不可能将获取消息的 HTTP 请求与遥测的其余部分相关联,特别是当收到多个消息时。

public async Task<MessagePayload> Dequeue(CloudQueue queue)
{
    var operation = telemetryClient.StartOperation<DependencyTelemetry>("dequeue " + queue.Name);
    operation.Telemetry.Type = "Azure queue";
    operation.Telemetry.Data = "Dequeue " + queue.Name;
    
    try
    {
        var message = await queue.GetMessageAsync();
    }
    catch (StorageException e)
    {
        operation.telemetry.Properties.Add("AzureServiceRequestID", e.RequestInformation.ServiceRequestID);
        operation.telemetry.Success = false;
        operation.telemetry.ResultCode = e.RequestInformation.HttpStatusCode.ToString();
        telemetryClient.TrackException(e);
    }
    finally
    {
        // Update status code and success as appropriate.
        telemetryClient.StopOperation(operation);
    }

    return null;
}

进程

在以下示例中,通过类似于跟踪传入 HTTP 请求的方式跟踪传入消息:

public async Task Process(MessagePayload message)
{
    // After the message is dequeued from the queue, create RequestTelemetry to track its processing.
    RequestTelemetry requestTelemetry = new RequestTelemetry { Name = "process " + queueName };
    
    // It might also make sense to get the name from the message.
    requestTelemetry.Context.Operation.Id = message.RootId;
    requestTelemetry.Context.Operation.ParentId = message.ParentId;

    var operation = telemetryClient.StartOperation(requestTelemetry);

    try
    {
        await ProcessMessage();
    }
    catch (Exception e)
    {
        telemetryClient.TrackException(e);
        throw;
    }
    finally
    {
        // Update status code and success as appropriate.
        telemetryClient.StopOperation(operation);
    }
}

同样,可以检测其他队列操作。 应该以类似于取消排队操作的方式检测速览操作。 不必检测队列管理操作。 Application Insights 会跟踪 HTTP 之类的操作,在大多数情况下,这就足够了。

检测消息删除时,请务必设置操作(关联)标识符。 或者,可以使用 Activity API。 这样就无需在遥测项目上设置操作标识符,因为 Application Insights SDK 会为用户完成:

  • 从队列中获取项目后,创建新的 Activity
  • 使用 Activity.SetParentId(message.ParentId) 关联使用者日志和生产者日志。
  • 启动 Activity
  • 使用 Start/StopOperation 帮助程序跟踪取消排队、处理和删除操作。 从同一个异步控制流(执行上下文)执行。 这样,它们就能正确关联。
  • 停止 Activity
  • 使用 Start/StopOperation 或手动调用 Track 遥测。

依赖项类型

Application Insights 使用依赖项类型来自定义 UI 体验。 对于队列,它识别以下可改善事务诊断体验DependencyTelemetry 类型:

  • Azure queue(Azure 存储队列)
  • Azure Event Hubs 适用于 Azure 事件中心
  • Azure Service Bus 适用于 Azure 服务总线

批处理

有些队列允许对一个请求的多条消息取消排队。 这类消息的处理可能彼此独立,而且属于不同的逻辑操作。 无法将 Dequeue 操作与正在处理的特定消息相关联。

每条消息都应在自己的异步控制流中处理。 有关详细信息,请参阅传出依赖项跟踪部分。

长时间运行后台任务

某些应用程序可能因用户请求而启动长时间运行的操作。 从跟踪/检测的角度来看,它与请求或依赖项检测没有区别:

async Task BackgroundTask()
{
    var operation = telemetryClient.StartOperation<DependencyTelemetry>(taskName);
    operation.Telemetry.Type = "Background";
    try
    {
        int progress = 0;
        while (progress < 100)
        {
            // Process the task.
            telemetryClient.TrackTrace($"done {progress++}%");
        }
        // Update status code and success as appropriate.
    }
    catch (Exception e)
    {
        telemetryClient.TrackException(e);
        // Update status code and success as appropriate.
        throw;
    }
    finally
    {
        telemetryClient.StopOperation(operation);
    }
}

在此示例中,telemetryClient.StartOperation 创建 DependencyTelemetry 并填充相关上下文。 假设有一个父操作,它是由计划操作的传入请求创建的。 只要在与传入请求相同的异步控制流中启动 BackgroundTask,它就会与该父操作相关联。 BackgroundTask 和所有嵌套的遥测项自动与引发此项的请求相关联,即使请求结束也一样。

从不含与之关联的任何操作 (Activity) 的后台线程启动任务时,BackgroundTask 没有任何父级。 但是,它可以具有嵌套操作。 从任务报告的所有遥测项与 BackgroundTask 中创建的 DependencyTelemetry 相关联。

传出依赖项跟踪

用户可以跟踪自己的依赖项类型或不受 Application Insights 支持的操作。

服务总线队列或 Azure 存储队列中的 Enqueue 方法可作为此类自定义跟踪的示例。

自定义依赖项跟踪的常规方法是:

  • 调用 TelemetryClient.StartOperation(扩展)方法,该方法填充关联所需的 DependencyTelemetry 属性和某些其他属性,例如开始时间、时间戳和持续时间。
  • DependencyTelemetry 上设置其他自定义属性,比如名称和所需的任何其他上下文。
  • 进行依赖项调用,并等它完成。
  • 完成后,使用 StopOperation 停止操作。
  • 处理异常。
public async Task RunMyTaskAsync()
{
    using (var operation = telemetryClient.StartOperation<DependencyTelemetry>("task 1"))
    {
        try 
        {
            var myTask = await StartMyTaskAsync();
            // Update status code and success as appropriate.
        }
        catch(...) 
        {
            // Update status code and success as appropriate.
        }
    }
}

释放某个操作会导致该操作停止,因此可以这样做而不是调用 StopOperation

警告

在某些情况下,未经处理的异常可能会阻止调用 finally,因此可能无法跟踪操作。

并行处理和跟踪操作

调用 StopOperation 只会停止已启动的操作。 如果当前运行的操作与要停止的操作不匹配,StopOperation 不执行任何操作。 如果在同一执行上下文中并行启动多个操作,则可能发生这种情况。

var firstOperation = telemetryClient.StartOperation<DependencyTelemetry>("task 1");
var firstTask = RunMyTaskAsync();

var secondOperation = telemetryClient.StartOperation<DependencyTelemetry>("task 2");
var secondTask = RunMyTaskAsync();

await firstTask;

// FAILURE!!! This will do nothing and will not report telemetry for the first operation
// as currently secondOperation is active.
telemetryClient.StopOperation(firstOperation); 

await secondTask;

请确保始终在同一异步方法中调用 StartOperation 和处理操作,以隔离并行运行的操作。 如果操作是同步的(或非异步的),请包装进程并使用 Task.Run 跟踪。

public void RunMyTask(string name)
{
    using (var operation = telemetryClient.StartOperation<DependencyTelemetry>(name))
    {
        Process();
        // Update status code and success as appropriate.
    }
}

public async Task RunAllTasks()
{
    var task1 = Task.Run(() => RunMyTask("task 1"));
    var task2 = Task.Run(() => RunMyTask("task 2"));
    
    await Task.WhenAll(task1, task2);
}

ApplicationInsights 操作与 System.Diagnostics.Activity

System.Diagnostics.Activity 表示分布式跟踪上下文,可供框架和库用于在进程内外创建和传播上下文,并关联遥测项。 ActivitySystem.Diagnostics.DiagnosticSource 配合运行,后者是框架/库之间的通知机制,用于通知需要关注的事件,例如传入或传出请求和异常。

活动是 Application Insights 中的一等公民。 自动依赖项和请求集合严重依赖于活动以及 DiagnosticSource 事件。 如果你在应用程序中创建了 Activity,将不会导致创建 Application Insights 遥测。 Application Insights 需要接收 DiagnosticSource 事件并了解事件名称和有效负载,然后才能将 Activity 转换为遥测。

每个 Application Insights 操作(请求或依赖项)都涉及 Activity。 调用 StartOperation 时,它会在下面创建 ActivityStartOperation 是建议的方法,用于手动跟踪请求或依赖项遥测,并确保一切都已关联。

后续步骤