Integration

Monitoring Dataverse integrations end to end: Application Insights, correlation IDs and managed identities

October 25, 2026
Sabhat Qureshi
16 min read

The question nobody can answer at nine in the morning

A sales representative raises a customer's credit limit in Dynamics 365. Half an hour later the ERP still shows the old value. The Dynamics team says the record saved fine, the Azure team says the function is green, and the ERP team says nothing arrived. Each of them is right about their own piece, and nobody can prove what happened to that one change.

In this article I walk through how I set up end-to-end monitoring for Dataverse integrations built on Azure Integration Services: getting telemetry out of Dataverse, instrumenting Service Bus, Azure Functions and Logic Apps, following one business transaction across every hop with a correlation ID, and alerting on what matters. The second half covers managed identities, because the same integrations are usually full of connection strings and client secrets that nobody wants to rotate. I stick to what Microsoft documents today and call out where something is in preview or has limits. For the Dataverse side of the architecture, start with the documentation on Azure integration with Microsoft Dataverse.

Why: one change, five hops, three teams

A typical event-driven Dynamics 365 integration has more moving parts than the whiteboard diagram suggests:

  1. Dataverse: a user or process changes a row, and plugins run inside the event pipeline.
  2. Plugin or service endpoint: a registered step posts the execution context to Azure Service Bus, either synchronously or through an asynchronous system job.
  3. Service Bus topic: the message waits in a subscription until a consumer picks it up, is retried, or ends up in the dead-letter queue.
  4. Azure Function or Logic App: the consumer maps, enriches and validates the data, often calling back into Dataverse for extra context.
  5. Target system: an ERP, a data platform or a partner API accepts or rejects the update.

Each hop has its own logs, its own retention and often its own owner. Without shared telemetry, a failure in hop 4 looks like a Dynamics problem to the business, because that is where they made the change. I have seen teams spend days discussing whose fault a missing update was, when one query across the right tables would have settled it in minutes.

The goal

For any business change, whoever is on support should be able to answer three questions within a few minutes: did it leave Dataverse, where is it now, and if it failed, why? Everything in this article serves those three questions.

The telemetry design in four decisions

Before configuring anything, I agree on four design decisions with the people who will support the integration:

  • One Log Analytics workspace per environment tier. Workspace-based Application Insights stores its data in a Log Analytics workspace, so Application Insights telemetry, Service Bus resource logs and Logic Apps logs can all be queried in one place.
  • One Application Insights resource per Dataverse environment for the platform export. Microsoft states that the out-of-the-box reports don't work correctly when one Application Insights resource holds data from several environments, and that the integration requires local authentication on that resource.
  • A separate Application Insights resource for the Azure components. That lets you move the Azure side to Microsoft Entra-only ingestion without breaking the Dataverse export, while both resources still write to the same workspace.
  • One business correlation ID, named and formatted the same way everywhere. Platform operation IDs change at every hop that isn't connected by W3C trace context. A correlation ID that you carry yourself survives all of them.

The fourth decision matters most. As far as I can find in the documentation, Dataverse doesn't put a W3C trace context on the Service Bus messages that a service endpoint sends, so the platform's own operation IDs won't stitch Dataverse and Azure together for you. The correlation ID does.

Dataverse side: exporting platform telemetry

The Power Platform admin center can export telemetry to Application Insights under Manage > Data export > App Insights. You choose the data type (Dataverse diagnostics and performance, Power Automate or Copilot Studio), filters, the source environment and the target Application Insights resource. Microsoft describes the setup in Export data to Application Insights.

Prerequisites to check first

  • Managed Environments: the feature is turned on and supported for managed environments only. Making an environment managed has licensing consequences: all active usage then requires qualifying standalone licenses (several Dynamics 365 Enterprise licenses qualify), and Microsoft has announced that from February 2027 users without an appropriate license can no longer open apps in managed environments. Check this before you promise telemetry. See Licensing requirements for managed environments.
  • Admin roles on both sides: the person who creates the export needs the Power Platform administrator or Dynamics 365 administrator role at tenant level, the System Administrator or environment admin role in the environment, and Contributor or Owner on the Application Insights resource. Microsoft lists insufficient permissions as a common cause of missing telemetry.
  • Local authentication stays on: the integration requires local authentication on the Application Insights resource, so don't disable it on the resource that receives the Dataverse export.
  • Expect latency: data starts flowing within 24 hours of setup, and the documented service level for delivering these telemetry streams is 24 hours. Treat it as a diagnostics feed, not a real-time alerting feed.
  • Preview scope: exporting Power Automate and Dynamics 365 Customer Service telemetry is in public preview and isn't available in GCC, GCC High or 21Vianet.

For Dataverse, the export contains incoming Web API and Organization service calls in the requests table, plugin executions (type Plugin) and SDK calls in the dependencies table, and plugin and SDK failures in the exceptions table, with custom dimensions such as the step name, depth and entity name. Any outbound call made by a plugin is logged as a dependency as well. The details are in Telemetry events for Microsoft Dataverse.

What the export doesn't give you

It tells you that a plugin ran, how long it took and whether it threw an exception. It doesn't tell you what your code decided or which record it handed to Service Bus. For that, the plugin has to write its own telemetry.

Plugin logging: ILogger and ITracingService side by side

Dataverse gives plugins two logging channels, and they end up in different places. Microsoft's guidance is to use both, as explained in Write telemetry to your Application Insights resource using ILogger.

CriterionILogger (Microsoft.Xrm.Sdk.PluginTelemetry)ITracingService
Where it ends upYour Application Insights resource (traces and exceptions), written directly and never sent to MicrosoftThe Plugin Trace Log table, the error dialog for synchronous steps, or the system job for asynchronous steps
RequiresThe Application Insights export from the admin center, and therefore a managed environment; otherwise nothing is writtenPlugin trace logging set to Exception or All in the environment
RetentionYour Application Insights retention (90 days by default)24 hours; a daily bulk delete job removes older records
VolumeA string per log call10 KB of text per execution; older lines are dropped beyond that
Who can read itAnyone with read access to the Application Insights resource or the workspaceThe message block and the (secure) configuration columns are readable only by system administrators

In practice I keep ITracingService for debugging and for the error details that users and support see, and add ILogger for telemetry I want to query over weeks. Use the Microsoft.Xrm.Sdk.PluginTelemetry namespace: if you import Microsoft.Extensions.Logging instead, the ILogger instance is null. ILogger also isn't supported in a profiling or debug session of the Plugin Registration tool.

A plugin that stamps the correlation ID and posts to Service Bus

I prefer a small custom Azure-aware plugin over the out-of-the-box service endpoint step. It lets me write telemetry at the moment of hand-off and add the correlation ID to the shared variables of the execution context, which Microsoft documents as writable before the post. The service endpoint itself is registered with JSON as the message format.

using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.PluginTelemetry; // not Microsoft.Extensions.Logging

namespace Crx.Integration.Plugins
{
    // Registered asynchronously on Update of account (PostOperation).
    // Unsecure configuration = ID of the Service Bus service endpoint (message format: JSON).
    public class AccountPostUpdateErpSyncPlugin : IPlugin
    {
        private readonly Guid _serviceEndpointId;

        public AccountPostUpdateErpSyncPlugin(string unsecureConfig)
        {
            if (!Guid.TryParse(unsecureConfig, out _serviceEndpointId))
            {
                throw new InvalidPluginExecutionException("Service endpoint ID missing from the step configuration.");
            }
        }

        public void Execute(IServiceProvider serviceProvider)
        {
            var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
            var tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
            var logger = (ILogger)serviceProvider.GetService(typeof(ILogger));
            var notificationService = (IServiceEndpointNotificationService)serviceProvider.GetService(typeof(IServiceEndpointNotificationService));

            // One ID for the whole journey. Dataverse's own CorrelationId is a sensible default.
            var correlationId = context.CorrelationId.ToString();
            context.SharedVariables["crx_correlationid"] = correlationId;

            try
            {
                logger?.LogInformation("ERP sync: posting {0} {1}. CorrelationId {2}",
                    context.PrimaryEntityName, context.PrimaryEntityId, correlationId);
                tracingService.Trace($"ERP sync: posting {context.PrimaryEntityName} {context.PrimaryEntityId}. CorrelationId {correlationId}");

                // Keep this plug-in to "stamp, log, post": a failed async post re-runs the whole plug-in
                notificationService.Execute(new EntityReference("serviceendpoint", _serviceEndpointId), context);
            }
            catch (Exception ex)
            {
                logger?.LogError(ex, "ERP sync: post to Service Bus failed. CorrelationId {0}", correlationId);
                tracingService.Trace($"Error: {ex.Message}");
                throw new InvalidPluginExecutionException($"An error occurred: {ex.Message}");
            }
        }
    }
}

Two documented details shape this design. The execution context in the message body includes the CorrelationId and, for asynchronous steps, the OperationId of the system job, which helps with duplicate detection. And when an asynchronous post fails, the system job retries by running the entire plugin again, so the plugin should do nothing beyond changing the context, logging and posting. Keep in mind too that a synchronous post can't be recalled if the transaction rolls back afterwards. See Write a custom Azure-aware plugin.

Azure side: Functions, Logic Apps and Service Bus

On the Azure side, the aim is that every component writes to the same workspace, uses a recognizable role name and logs the correlation ID in a searchable way.

Azure Functions: OpenTelemetry and structured logs

For .NET isolated worker functions, Microsoft now documents OpenTelemetry as the way to send host and worker telemetry to Application Insights: set telemetryMode to OpenTelemetry in host.json and register the Functions worker OpenTelemetry integration with the Azure Monitor exporter in Program.cs. The setup is described in Use OpenTelemetry with Azure Functions.

// host.json
{
  "version": "2.0",
  "telemetryMode": "OpenTelemetry"
}

// Program.cs (.NET isolated worker)
// Packages: Microsoft.Azure.Functions.Worker.OpenTelemetry, OpenTelemetry.Extensions.Hosting,
//           Azure.Monitor.OpenTelemetry.Exporter, Azure.Identity, Azure.Messaging.ServiceBus
using Azure.Identity;
using Azure.Messaging.ServiceBus;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Azure.Functions.Worker.OpenTelemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = FunctionsApplication.CreateBuilder(args);
builder.ConfigureFunctionsWebApplication();

// The user-assigned identity of this integration (use DefaultAzureCredential for local development)
var credential = new ManagedIdentityCredential(builder.Configuration["UamiClientId"]);

builder.Services.AddOpenTelemetry()
    .UseFunctionsWorkerDefaults()
    .UseAzureMonitorExporter(options => options.Credential = credential); // Entra ID ingestion (worker)
builder.Logging.AddOpenTelemetry(options => options.IncludeScopes = true); // scopes are off by default

builder.Services.AddSingleton(new ServiceBusClient(
    builder.Configuration["ServiceBusConnection__fullyQualifiedNamespace"], credential));
builder.Services.AddSingleton(sp =>
    sp.GetRequiredService<ServiceBusClient>().CreateSender("erp-outbound"));

builder.Build().Run();

Three details from that documentation matter for integrations. Logging scopes aren't included by default, so enable IncludeScopes or put the correlation ID in the message template. The operation ID of a Service Bus-triggered invocation comes from the traceparent of the incoming message. And if you use Microsoft Entra authentication for ingestion, you configure it separately for the host (the APPLICATIONINSIGHTS_AUTHENTICATION_STRING setting) and for the worker (the exporter's credential). If you prefer the classic Application Insights SDK, the same principles apply.

Logic Apps: Standard versus Consumption

Standard logic apps run on the Azure Functions runtime and can send telemetry to Application Insights. With enhanced telemetry enabled in host.json, trigger and action events land in the requests table with the workflow name, run ID and client tracking ID in the custom dimensions; the operation ID equals the workflow run ID. See Enhanced telemetry for Standard workflows.

// host.json of a Standard logic app: enhanced Application Insights telemetry
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle.Workflows",
    "version": "[1, 2.00]"
  },
  "extensions": {
    "workflow": {
      "Settings": {
        "Runtime.ApplicationInsightTelemetryVersion": "v2"
      }
    }
  }
}

Consumption logic apps don't have that Application Insights integration: you add a diagnostic setting with the workflow runtime category that sends to a Log Analytics workspace, optionally with the Logic Apps Management solution. For Standard, the same kind of diagnostic setting writes to the LogicAppWorkflowRuntime table. On both plans, set the trigger's Custom Tracking Id to the correlation ID from the message, and use tracked properties for the business keys you search on. Microsoft notes that resource logs can take tens of minutes to arrive and aren't fully lossless, so treat them as diagnostics rather than an audit trail. See Collect diagnostic data for Logic Apps workflows.

Service Bus: metrics, resource logs and trace context

Service Bus provides platform metrics per entity without any setup: active messages, dead-lettered messages, incoming and outgoing messages, server errors, user errors and throttled requests. The dead-lettered and active message counts can't be exported to Log Analytics through a diagnostic setting, so alert on them with metric alerts. Resource logs (operational logs, diagnostic error logs, virtual network logs, application metrics logs and, on the Premium tier only, runtime audit logs and geo-replication logs) go to Log Analytics through a diagnostic setting, either into AzureDiagnostics or into resource-specific tables. The reference is Monitoring data reference for Azure Service Bus.

Operational logs only capture management operations, not sends and receives, so message-level tracing has to come from the clients. The Azure.Messaging.ServiceBus SDK carries W3C trace context in the Diagnostic-Id application property; that is how a function that sends a message and the function that receives it end up in the same Application Insights operation. See Service Bus end-to-end tracing and diagnostics.

The application map

Once the Azure components send telemetry with clear role names, the application map shows the hops your code can see: the function, its Service Bus and HTTP dependencies, and its calls back to Dataverse. Don't expect it to pull the Dataverse plugin into the same picture: the platform export lives in its own resource and isn't linked to your functions by trace context. That gap is exactly what the correlation ID closes. See Application map in Application Insights.

Following one business transaction across every hop

The pattern I use is simple and doesn't depend on the tracing support of any single product:

  1. The plugin takes the Dataverse CorrelationId (or an ID that arrived from an upstream API), writes it to the shared variables of the execution context and logs it with ILogger.
  2. The function reads it from the message body, tags the invocation with it and includes it in every log line.
  3. Every message the function sends downstream carries it as the Service Bus CorrelationId and as an application property.
  4. The Logic App uses it as Custom Tracking Id and as a tracked property.
  5. Calls to the target system pass it in a header, so the receiving team can search their own logs for the same value.

The function: read, tag, propagate

A Service Bus-triggered isolated worker function, using the client and sender registered in Program.cs above. The managed identity's client ID comes from app settings; nothing in the configuration is a secret.

using System.Diagnostics;
using System.Text.Json;
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;

public class ErpSyncFunction
{
    private readonly ILogger<ErpSyncFunction> _logger;
    private readonly ServiceBusSender _erpSender;

    public ErpSyncFunction(ILogger<ErpSyncFunction> logger, ServiceBusSender erpSender)
    {
        _logger = logger;
        _erpSender = erpSender;
    }

    [Function(nameof(ErpSyncFunction))]
    public async Task Run(
        [ServiceBusTrigger("crm-account-events", "erp-sync", Connection = "ServiceBusConnection")]
        ServiceBusReceivedMessage message)
    {
        // Body = the RemoteExecutionContext as JSON. Check the shape against a captured message first.
        using var body = JsonDocument.Parse(message.Body.ToString());
        var context = body.RootElement;
        var correlationId = ReadSharedVariable(context, "crx_correlationid")
            ?? context.GetProperty("CorrelationId").GetString()!;

        // Searchable on the request row and on every log line of this invocation
        Activity.Current?.SetTag("crx.correlation_id", correlationId);
        using var scope = _logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId });

        _logger.LogInformation("ERP sync started for {EntityName} {EntityId}",
            context.GetProperty("PrimaryEntityName").GetString(),
            context.GetProperty("PrimaryEntityId").GetString());

        // ... map and validate. Throw on failure: Service Bus retries and eventually dead-letters.

        var outbound = new ServiceBusMessage(BinaryData.FromObjectAsJson(new { correlationId /*, payload */ }))
        {
            CorrelationId = correlationId,
            ContentType = "application/json"
        };
        outbound.ApplicationProperties["crxCorrelationId"] = correlationId;
        await _erpSender.SendMessageAsync(outbound);
    }

    private static string? ReadSharedVariable(JsonElement context, string key)
    {
        if (!context.TryGetProperty("SharedVariables", out var variables)) return null;
        foreach (var pair in variables.EnumerateArray())
        {
            if (pair.GetProperty("key").GetString() == key) return pair.GetProperty("value").ToString();
        }
        return null;
    }
}

One query to follow a correlation ID

Run this in the Log Analytics workspace that both Application Insights resources and the Logic Apps diagnostic setting write to. It finds every operation that mentions the ID, collects all telemetry for those operations, adds the Logic App events and sorts everything in time order.

// Log Analytics workspace shared by both Application Insights resources and the Logic Apps diagnostic setting
let cid = "6f1c2d3e-5a4b-4c7d-9e8f-0a1b2c3d4e5f";
let lookback = ago(3d);
// 1. Every operation (plug-in, function invocation) that mentions the ID
let ops = union AppTraces, AppRequests, AppExceptions
    | where TimeGenerated > lookback
    | where Message has cid or tostring(Properties) has cid
    | distinct OperationId;
// 2. All Dataverse and Function telemetry belonging to those operations
let appTelemetry = union AppRequests, AppDependencies, AppTraces, AppExceptions
    | where TimeGenerated > lookback and OperationId in (ops)
    | project TimeGenerated, Hop = AppRoleName, Kind = Type,
              Detail = coalesce(Name, Message, OuterMessage), Success, DurationMs, OperationId;
// 3. Logic App events that carry the ID as client tracking ID
let workflowEvents = LogicAppWorkflowRuntime
    | where TimeGenerated > lookback and ClientTrackingId == cid
    | project TimeGenerated, Hop = WorkflowName, Kind = OperationName,
              Detail = coalesce(ActionName, TriggerName, Error), Success = Status != "Failed",
              DurationMs = real(null), OperationId = RunId;
union appTelemetry, workflowEvents
| order by TimeGenerated asc

Save it as a shared query or a workbook with the correlation ID as a parameter, and give support staff read access to the workspace. The first time someone answers 'where is my update?' in two minutes without escalating, the effort has paid off.

Alerts worth having

I keep the first set of alerts small and route each one to the team that can act on it:

SignalSourceRule
Dead-lettered messagesService Bus metric DeadletteredMessages, split by entityMaximum above 0 over 5 minutes. Every dead-lettered message is a business change that didn't arrive.
Growing backlogService Bus metric ActiveMessages, split by entityAbove a threshold based on your normal volume for 15 minutes or longer: the consumer is down, throttled or too slow.
Failed function executionsLog alert on AppRequests from the integration's Application InsightsAny request with Success equal to false, grouped by function, per 5 minutes.
Failed workflow runsConsumption: the Runs Failed metric. Standard: a log alert on LogicAppWorkflowRuntimeAny run that completes with status Failed.
Plugin failuresAppDependencies (type Plugin) and AppExceptions from the Dataverse exportUse for trends and daily review rather than paging, given the documented 24-hour delivery service level.
Latency and silenceFunction request duration and the count of successful runs95th percentile above the processing time you agreed with the business, and zero successful runs during business hours when you expect traffic.
// Failed function executions (integration Application Insights / workspace)
AppRequests
| where Success == false
| summarize Failures = count() by AppRoleName, OperationName, bin(TimeGenerated, 5m)

// Failed Standard workflow runs (Logic Apps diagnostic setting)
LogicAppWorkflowRuntime
| where OperationName has "WorkflowRunCompleted" and Status == "Failed"
| summarize FailedRuns = count() by WorkflowName, bin(TimeGenerated, 5m)

// Failing plug-ins (Dataverse export, for trends rather than paging)
AppDependencies
| where DependencyType == "Plugin" and Success == false
| summarize Failures = count() by Name, bin(TimeGenerated, 1h)

Send every alert to an action group with a named owner, and put a link to the correlation query in the alert description. An alert that nobody owns turns into noise within weeks.

Making it secret-free with managed identities

Monitoring tells you when an integration breaks. Managed identities remove one of the most common reasons it breaks: an expired or leaked secret. A managed identity is a Microsoft Entra identity whose credentials Azure manages for you. See What are managed identities for Azure resources?.

System-assigned or user-assigned?

  • System-assigned: created with the resource and deleted with it, one per resource. Simple, but recreating the resource gives you a new principal, and you can't grant roles or create the Dataverse application user before the resource exists.
  • User-assigned: a standalone resource that you create once and attach to one or more apps. You can grant roles and register it in Dataverse before deploying any code, and it survives redeployments.

For integrations I normally use one user-assigned identity per integration per environment, so the permissions of the production ERP sync are visible in one place and are never shared with test. The exception is a Standard logic app that uses built-in service provider connectors such as Service Bus: Microsoft notes that most of these currently support only the system-assigned identity.

Functions to Service Bus, storage and Application Insights without connection strings

An identity-based connection in Functions is a group of app settings that share a prefix. For Service Bus you set <prefix>__fullyQualifiedNamespace and, for a user-assigned identity, <prefix>__credential set to managedidentity and <prefix>__clientId. The host storage account and Application Insights have equivalent settings. Microsoft documents them in Configure connections in Azure Functions.

# Function app settings: identifiers only, no secrets
ServiceBusConnection__fullyQualifiedNamespace = sb-crx-integration.servicebus.windows.net
ServiceBusConnection__credential              = managedidentity
ServiceBusConnection__clientId                = <client ID of id-crx-erp-sync>
AzureWebJobsStorage__accountName              = stcrxerpsync
AzureWebJobsStorage__credential               = managedidentity
AzureWebJobsStorage__clientId                 = <client ID of id-crx-erp-sync>
APPLICATIONINSIGHTS_CONNECTION_STRING         = InstrumentationKey=...;IngestionEndpoint=...
APPLICATIONINSIGHTS_AUTHENTICATION_STRING     = ClientId=<client ID of id-crx-erp-sync>;Authorization=AAD
UamiClientId                                  = <client ID of id-crx-erp-sync>
DataverseUrl                                  = https://yourorg.crm4.dynamics.com
ErpApiKey                                     = @Microsoft.KeyVault(SecretUri=https://kv-crx-integration.vault.azure.net/secrets/erp-api-key/)

# Role assignments at the narrowest scope (Azure CLI)
MI=$(az identity show -g rg-crx-integration -n id-crx-erp-sync --query principalId -o tsv)
SB=$(az servicebus namespace show -g rg-crx-integration -n sb-crx-integration --query id -o tsv)

az role assignment create --assignee-object-id $MI --assignee-principal-type ServicePrincipal \
  --role "Azure Service Bus Data Receiver" --scope "$SB/topics/crm-account-events/subscriptions/erp-sync"
az role assignment create --assignee-object-id $MI --assignee-principal-type ServicePrincipal \
  --role "Azure Service Bus Data Sender" --scope "$SB/queues/erp-outbound"
az role assignment create --assignee-object-id $MI --assignee-principal-type ServicePrincipal \
  --role "Key Vault Secrets User" \
  --scope $(az keyvault show -g rg-crx-integration -n kv-crx-integration --query id -o tsv)
az role assignment create --assignee-object-id $MI --assignee-principal-type ServicePrincipal \
  --role "Monitoring Metrics Publisher" \
  --scope $(az monitor app-insights component show -g rg-crx-integration -a appi-crx-integration --query id -o tsv)

# Key Vault references must use the user-assigned identity, not the system-assigned one
az functionapp update -g rg-crx-integration -n func-crx-erp-sync \
  --set keyVaultReferenceIdentity=$(az identity show -g rg-crx-integration -n id-crx-erp-sync --query id -o tsv)

Grant roles at the narrowest scope that works: Azure Service Bus Data Receiver on the topic subscription the function listens to, and Azure Service Bus Data Sender on the queue or topic it writes to. One catch from the Service Bus trigger documentation: for accurate scaling, the extension reads entity runtime properties, which requires the Azure Service Bus Data Owner role or a custom role that includes Microsoft.ServiceBus/namespaces/*/read. Without it there's no error at start-up; scaling quietly falls back to a less accurate estimate. Role assignments can take up to five minutes to propagate, and the Azure portal can't assign roles at topic subscription level, so use the CLI or Bicep. See Use managed identities to access Azure Service Bus.

Logic Apps to Service Bus and Key Vault

Logic Apps supports managed identities on both hosting plans: the managed Service Bus and Key Vault connectors and the built-in HTTP action work with them on Consumption and Standard, and Standard adds the built-in Service Bus connector. A logic app can have several identities, but each connection uses one at a time. See Authenticate with managed identities in Azure Logic Apps.

Functions and Logic Apps calling Dataverse as a managed identity

Dataverse accepts a managed identity as an application user. In the Power Platform admin center, open the environment, go to Settings > Users + permissions > Application users, choose New app user and enter the managed identity's application ID (not its name). Then assign a business unit and a least-privilege security role. See Manage application users in the Power Platform admin center.

In a function, give the Dataverse ServiceClient a token provider that asks the managed identity for a token for the environment URL. This continues the Program.cs shown earlier:

// Program.cs: Dataverse as the managed identity (registered as an application user)
// Packages: Microsoft.PowerPlatform.Dataverse.Client, Azure.Identity
using Azure.Core;
using Microsoft.PowerPlatform.Dataverse.Client;

var dataverseUrl = new Uri(builder.Configuration["DataverseUrl"]!);
var dataverseScope = $"{dataverseUrl.GetLeftPart(UriPartial.Authority)}/.default";

builder.Services.AddSingleton(sp => new ServiceClient(
    dataverseUrl,
    async _ => (await credential.GetTokenAsync(
        new TokenRequestContext(new[] { dataverseScope }), CancellationToken.None)).Token,
    useUniqueInstance: true,
    logger: sp.GetRequiredService<ILogger<ServiceClient>>()));

For Logic Apps there is a catch: the Microsoft Dataverse connector currently lists OAuth, service principal with a client secret and client certificate as its authentication types, not managed identity. When I want a logic app to call Dataverse without a secret, I use the HTTP action against the Dataverse Web API with managed identity authentication and the environment URL as the audience, and register that identity as an application user as described above. See the authentication types on the Microsoft Dataverse connector reference.

Managed identity for Dataverse plugins

Plugins can obtain tokens for Azure resources without stored credentials through Power Platform managed identity. Microsoft recommends version 2 of the setup, which consists of these steps:

  1. Create a user-assigned managed identity or an app registration in Microsoft Entra ID and note its client ID and tenant ID.
  2. Call IManagedIdentityService.AcquireToken in the plugin, sign the assembly (or plugin package) with a certificate and register it.
  3. On the identity, add a federated identity credential with issuer https://login.microsoftonline.com/{tenantId}/v2.0 and a subject identifier built from the encoded tenant ID, the environment ID and hashes of the certificate's issuer and subject.
  4. Create a managedidentity record in Dataverse through the Web API with version 2 and bind the plugin assembly or package to it.
  5. Grant the identity access to the Azure resource, for example the Key Vault Secrets User role on a key vault.
  6. Trigger the plugin and confirm that it acquires a token.
// Inside Execute(): read a secret from Key Vault as the plug-in's managed identity
var identityService = (IManagedIdentityService)serviceProvider.GetService(typeof(IManagedIdentityService));
var token = identityService.AcquireToken(new[] { "https://vault.azure.net/.default" });

using (var client = new HttpClient())
{
    client.Timeout = TimeSpan.FromSeconds(15);
    client.DefaultRequestHeaders.ConnectionClose = true; // KeepAlive false, per plug-in guidance
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

    var response = client
        .GetAsync("https://kv-crx-integration.vault.azure.net/secrets/erp-api-key?api-version=7.4")
        .GetAwaiter()
        .GetResult();
    response.EnsureSuccessStatusCode();
    // Read "value" from the JSON body. Never trace or log the secret itself.
}

Know the limitations before you plan on it. Self-signed certificates are for development and testing only. The subject identifier contains the environment ID, so every environment needs its own federated credential and its own managedidentity record, which belongs in your deployment scripts rather than in someone's memory. The managed identity commands of the Power Platform CLI don't work with user-assigned identities or on Linux. The setup guide is Set up Power Platform managed identity for Dataverse plugins.

What still needs a secret, and where to keep it

Going fully secret-free is rarely possible on day one. These are the secrets I still find most often:

  • The Dataverse service endpoint to Service Bus: the documented way for Dataverse to post to Service Bus is still a shared access signature. Use a dedicated policy with Send rights only, on that one topic, and rotate it. The ServiceEndpoint table schema now shows a Managed Identity authentication type and Key Vault-related columns, but I haven't found a documented setup procedure for them, so I don't rely on them in production yet.
  • Target systems outside Microsoft Entra ID: API keys or OAuth client secrets for an ERP or a partner API. Store them in Key Vault and reference them from app settings with @Microsoft.KeyVault(...), which the app resolves with its managed identity.
  • The Dataverse connector in Logic Apps: if you use it instead of the HTTP action, its client secret or certificate belongs in Key Vault, with an owner and an expiry date in someone's calendar.
  • Plugins calling non-Azure APIs: rather than putting a key in the secure configuration of a step, let the plugin's managed identity read it from Key Vault at runtime, and never write it to a trace.
  • Inbound webhooks: Dataverse webhooks authenticate with a key, an HTTP header or a query string, so an HTTP-triggered function behind a webhook needs a function key or similar. Keep it in Key Vault and in the webhook registration, nowhere else.

For Key Vault itself, use the Azure RBAC permission model and give each identity the Key Vault Secrets User role on the vault (or on individual secrets). When a function app or Standard logic app resolves Key Vault references with a user-assigned identity, set keyVaultReferenceIdentity on the app; otherwise it uses the system-assigned identity. See Use Key Vault references in App Service and Azure Functions.

Rollout checklist

  1. Draw the hops of each integration with an owner per hop, and agree on the name and format of the correlation ID.
  2. Create a Log Analytics workspace per environment tier, with workspace-based Application Insights: one resource per Dataverse environment for the platform export, and one for the Azure components.
  3. Check licensing before enabling Managed Environments, then configure the Application Insights export with an account that holds all required roles, and allow 24 hours for the first data.
  4. Add ILogger next to ITracingService in the plugins on the integration path, and keep plugin trace logging on Exception in production.
  5. Register the service endpoint with JSON as the message format and post through a plugin that stamps the correlation ID.
  6. Enable OpenTelemetry in Functions, log structured values, tag the invocation and propagate the ID on every outbound message and HTTP call.
  7. Enable enhanced telemetry (Standard) or a diagnostic setting (Consumption) for Logic Apps, and set Custom Tracking Id and tracked properties.
  8. Add Service Bus diagnostic settings and metric alerts for dead-lettered messages and backlog, plus log alerts for failed functions and workflow runs.
  9. Create one user-assigned identity per integration per environment, assign roles at entity scope, register it as a Dataverse application user with a least-privilege role, and move every remaining secret into Key Vault.
  10. Save the correlation query as a workbook, then break something on purpose in test and check that support can trace it within minutes.

Common pitfalls

  • Sampling away the evidence. Integrations often have low volumes where every message matters. Check your sampling settings: with parent-based sampling, the documentation notes that request telemetry isn't generated when the incoming message isn't sampled.
  • Disabling local authentication on the wrong resource. The Dataverse export needs local authentication on its Application Insights resource. Enforce Entra-only ingestion on the Azure-side resource instead.
  • Disabling SAS on the Service Bus namespace too early. If the Dataverse service endpoint still uses a shared access signature, disabling local authentication on the namespace stops Dataverse from posting. Inventory every SAS consumer first.
  • One Application Insights resource for several Dataverse environments. Microsoft states that the out-of-the-box reports then don't work correctly, and mixing test and production telemetry makes every query harder.
  • Expecting Dataverse telemetry in real time. The documented delivery service level is 24 hours. Page on the Azure-side signals and use the Dataverse export for diagnosis and trends.
  • Receiver-only permissions on a scaling function. Without the read permission described above, scaling silently becomes less accurate. On Standard logic apps, also remember that most built-in connectors only work with the system-assigned identity.
  • Logging personal data. Application Insights is read by more people than the plugin trace log, whose sensitive columns are restricted to System Administrators. Log record IDs and correlation IDs, not names, email addresses or payloads; under GDPR, telemetry is personal data processing too.
  • Forgetting token lifetimes and propagation. New role assignments can take up to five minutes to apply, and after you remove a role a running app can keep using its existing Service Bus token until it expires, which can take up to 24 hours. Restart the app when you revoke access.

Conclusion

End-to-end monitoring of a Dataverse integration isn't a product you switch on. It's a handful of decisions applied consistently: export what Dataverse offers, write your own telemetry where the platform stops, carry one correlation ID through every hop, and alert on the few signals that mean a business change didn't arrive. Managed identities then remove a whole category of incidents. Start with one integration, make it traceable from Dynamics 365 to the target system, and use it as the template for the rest.

Need help with your Dynamics 365 project?

I help organizations make their Dynamics 365 integrations observable and secret-free, from Application Insights and alerting design to rolling out managed identities across Dataverse, Service Bus, Functions and Logic Apps.