DevOps · Devops

Acumatica Distributed Tracing

Acumatica Distributed Tracing is the Acumatica operations topic that you only get good at by doing it badly a few times.

John Kihiu12 min read

A single Sales Order in Acumatica can trigger a webhook, fire a business event into a message bus, hit a tax fiscalization API, and receive an asynchronous callback that updates the order again — four systems, four log files, and no obvious way to tell which webhook call caused which downstream error six hours after the fact. Distributed tracing exists to answer exactly that question: not "is the system slow" but "which hop broke, for this one transaction, right now."

Why Acumatica integrations need tracing

Acumatica's own audit trail and trace logs are excellent for what happens inside Acumatica — field changes, who touched a record, which business event fired. They stop being useful the moment a transaction leaves the instance. A webhook fired from a business event, picked up by middleware (Azure Service Bus, n8n, a custom integration service), forwarded to a tax fiscalization API, and returned via callback is four separate systems each logging in isolation. Without a shared identifier threading through all four, debugging a failed transaction means manually correlating timestamps across log sources that don't agree on clock skew, format, or retention — the kind of task that eats an afternoon for what should be a five-minute lookup.

Correlating trace IDs across business events and middleware

The fix is a trace ID generated once, at the first hop, and carried through every subsequent call. Acumatica doesn't generate W3C trace context natively, so the practical pattern is to mint the trace ID in the business event handler or the outbound webhook code (a GUID is enough if you're not adopting full OpenTelemetry; a proper W3C traceparent header if you are), store it against the source record — a custom field on the Business Event's target entity or on a custom interface log DAC — and pass it as an HTTP header on every outbound call. Middleware like Azure Service Bus or n8n should propagate that same header unchanged rather than generating a new ID per hop; the moment a hop mints a fresh ID instead of forwarding the one it received, the trace breaks and you're back to manual correlation.

Log the trace ID on the Acumatica side too

A trace is only as useful as its weakest link. If the external fiscalization service logs trace IDs beautifully but Acumatica's side only logs "webhook sent" with no correlation ID, you still can't jump from the Acumatica record to the external system's logs. A simple UsrTraceId field on the interface log or business event log, populated at send time, closes that gap cheaply.

OpenTelemetry conventions and where to inject context

For anything beyond a couple of integration points, standardizing on OpenTelemetry's conventions pays off even if Acumatica itself isn't instrumented as an OTel source — the middleware and external services almost certainly can be. The traceparent header (W3C Trace Context) is the de facto standard: a 128-bit trace ID plus a per-hop span ID, propagated on every outbound HTTP call. The practical injection points for an Acumatica-centered flow: on the outbound webhook call fired from a business event (custom code in the event handler sets the header), on the middleware's outbound call to the external API (Service Bus message properties or n8n's HTTP node headers), and on the callback the external system sends back to Acumatica's REST/webhook receiver (read the incoming header and log it against the same trace ID before processing).

C# · TRACE CONTEXT PROPAGATION
public class SOOrderWebhookHandler
{
    public async Task SendFiscalizationRequest(SOOrder order, string parentTraceId)
    {
        var traceId = string.IsNullOrEmpty(parentTraceId)
            ? Guid.NewGuid().ToString("N")
            : parentTraceId;
        var spanId = Guid.NewGuid().ToString("N").Substring(0, 16);

        using var client = new HttpClient();
        var request = new HttpRequestMessage(HttpMethod.Post, fiscalizationEndpoint)
        {
            Content = JsonContent.Create(new { orderNbr = order.OrderNbr, amount = order.OrderTotal })
        };
        request.Headers.Add("traceparent", $"00-{traceId}-{spanId}-01");

        // Persist for correlation before the call, in case the callback
        // arrives after this process has moved on
        var log = new InterfaceLog
        {
            OrderNbr = order.OrderNbr,
            TraceId = traceId,
            Direction = "OUTBOUND",
            Endpoint = fiscalizationEndpoint
        };
        interfaceLogGraph.Logs.Insert(log);
        interfaceLogGraph.Save.Press();

        var response = await client.SendAsync(request);
        // response handling / retry logic omitted
    }
}

Debugging a failed multi-hop transaction

With trace IDs threaded through, debugging a stuck order becomes a lookup instead of a log-diffing exercise: an SO created in Acumatica fires a business event, which fires a webhook carrying traceparent; the receiving middleware logs the same trace ID against its own span before calling the fiscalization API; the fiscalization API's response (or timeout) is logged against that trace ID too; and the callback that updates Acumatica reads the trace ID back out of the response payload or a correlation header so the final update ties back to the original order. When something breaks, you search every system's logs for one trace ID and get the full chain in order, including which hop stalled or errored — instead of grepping four log sources by approximate timestamp and hoping nothing else fired in that window.

Callbacks need the trace ID even when they can't get it for free

Some fiscalization and tax APIs don't echo back a custom trace header — they only return their own reference number. In that case, persist a mapping (external reference number to your trace ID) at send time, the same interface log row works, so the callback handler can look up the trace ID by the reference number instead of losing the thread entirely.

Wrapping up

Distributed tracing for Acumatica integrations isn't about instrumenting Acumatica itself with a tracing SDK — it's about generating one ID early, logging it on both sides of every hop (including inside Acumatica via a custom field), and propagating it faithfully through whatever middleware sits between Acumatica and the outside world. The payoff shows up exactly when you need it most: a customer complaint about a stuck order becomes a single trace-ID search instead of an afternoon spent cross-referencing timestamps across systems that don't share a clock.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.