End-to-End Tracking in Azure Functions

Implement end-to-end request tracking with a TrackingID in Azure Functions, carrying the ID between services as a Service Bus application property.

A single request now passes through more services than anyone can trace by hand. An API starts a background job, a message lands on a queue, and something downstream of that queue picks it up long after the original request has finished.

The decoupling buys scalability and costs visibility. When something breaks, following one user’s request across a dozen services is the hard part.

The fix is a correlation ID that travels with the work, a TrackingID here. In Azure Functions talking over Service Bus, that means putting the ID on the message as an application property and logging it on every line.

Architecture at a Glance

  1. Producer Function: Receives an HTTP request, generates or extracts a TrackingID, sends a message to Service Bus with this ID as a custom property.
  2. Consumer Function: Listens to the Service Bus queue or topic, reads the TrackingID, and logs it on every line. It can optionally pass the ID further as needed.

0. Wiring Up the Sender

The sender is created once at startup and injected where it’s needed. Here’s Program.cs:

using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = FunctionsApplication.CreateBuilder(args);

builder.ConfigureFunctionsWebApplication();

builder.Services.AddAzureClients(clients =>
{
    clients.AddServiceBusClient(builder.Configuration["ServiceBusConnection"]);
});

// One sender for the life of the process. ServiceBusSender is thread-safe and
// reuses the AMQP link underneath, so don't build one per invocation.
builder.Services.AddSingleton(sp =>
    sp.GetRequiredService<ServiceBusClient>().CreateSender("orders"));

builder.Build().Run();

That needs Microsoft.Extensions.Azure, Azure.Messaging.ServiceBus, and Microsoft.Azure.Functions.Worker.Extensions.ServiceBus.

The [ServiceBusOutput] binding won’t do the job here. It sends the message body and nothing else, so it can’t set ApplicationProperties. Since that’s where the TrackingID travels, the message goes out through the SDK instead.

1. Producer Function: Sending a Message with TrackingID

using System.Net;
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;

public class ProducerFunction
{
    private readonly ILogger<ProducerFunction> _logger;
    private readonly ServiceBusSender _sender;

    public ProducerFunction(ILogger<ProducerFunction> logger, ServiceBusSender sender)
    {
        _logger = logger;
        _sender = sender;
    }

    [Function("ProducerFunction")]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        // ReadAsStringAsync handles the body. Reading req.Body with a StreamReader
        // is synchronous IO, and ASP.NET Core integration disallows that by default.
        string requestBody = await req.ReadAsStringAsync() ?? string.Empty;

        // Headers is an HttpHeadersCollection, so there's no string indexer here.
        // Read a header with TryGetValues.
        string trackingId = req.Headers.TryGetValues("TrackingID", out var values)
            ? values.FirstOrDefault() ?? Guid.NewGuid().ToString()
            : Guid.NewGuid().ToString();

        var message = new ServiceBusMessage(BinaryData.FromString(requestBody));
        message.ApplicationProperties["TrackingID"] = trackingId;

        _logger.LogInformation("Sending message with TrackingID {TrackingID}", trackingId);

        await _sender.SendMessageAsync(message);

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync(new
        {
            Status = "Message sent",
            TrackingID = trackingId
        });

        return response;
    }
}

The sender uses the Azure.Messaging.ServiceBus SDK, attaches TrackingID as a custom application property, and logs it as a structured property.

2. Consumer Function: Receiving and Logging with TrackingID

using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

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

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

    [Function("ConsumerFunction")]
    public void Run(
        [ServiceBusTrigger("orders", Connection = "ServiceBusConnection")]
        ServiceBusReceivedMessage message)
    {
        // Extract TrackingID from the message properties
        string trackingId = message.ApplicationProperties.TryGetValue("TrackingID", out var value)
            ? value?.ToString() ?? "N/A"
            : "N/A";

        _logger.LogInformation(
            "Received message {MessageId} with TrackingID {TrackingID}",
            message.MessageId, trackingId);

        _logger.LogInformation("Processing message body: {Body}", message.Body);

        // Pass trackingId downstream if needed -- for example, add it to outgoing
        // HTTP headers or include it in the audit row you just wrote.
    }
}

Correlating Logs

Log the TrackingID as a structured property on every line. Don’t bury it in the message text:

_logger.LogInformation("Processing message {MessageId} with TrackingID {TrackingID}",
    message.MessageId, trackingId);

Every named placeholder becomes its own field on the log record (customDimensions, if you’re on Application Insights). One query then gives you every line for a single request, across every service it passed through.

I’d skip BeginScope for this. Whether scope values reach your log store is down to the logging provider, not the Functions runtime, and when they don’t, the TrackingID is missing from the exact logs you went looking for it in.

Propagating TrackingID to Downstream Services

Once you’ve got the TrackingID, you can:

One ID across every service means one search returns the whole story for a request.

Conclusion

The whole pattern is three things:

There’s no new infrastructure to run, and the code cost is a few lines.

What’s Next?

Happy tracing!

#azure-functions #service-bus #distributed-systems
Reply to this post by email ↗