A production issue occurs, Plugin Trace Log is enabled, and after reproducing the problem you open the trace...
...only to discover that the beginning of the execution has disappeared.
The reason is simple: Plugin Trace Log has a maximum size of approximately 10 KB (10,240 characters).
Once that limit is exceeded, Dynamics 365 does not stop logging. Instead, it silently discards the oldest trace entries and keeps only the most recent ones.
For simple plugins this isn't an issue.
For complex plugins with multiple validation steps, service calls, and business logic, losing the beginning of the trace often means losing the most useful information.
In one of our projects we solved this problem by introducing a custom implementation of ITracingService that buffers all trace messages and decides what should eventually be written to Dynamics.
Understanding the Default Behavior
Imagine a plugin producing a trace like this:
Plugin started
Reading configuration
Loading Target
Checking security
Loading related records
...
...
...
Calling external API
Updating records
Plugin completed
When the trace exceeds 10 KB, Dynamics transforms it into something similar to:
...
...
...
Calling external API
Updating records
Plugin completed
The entire startup sequence disappears.
Unfortunately that's exactly the part we usually need while debugging.
The Solution
Instead of writing directly to the platform tracing service, every message is first stored in memory.
Only at the end of the execution do we decide how to write it.
The implementation has two responsibilities:
buffer every trace message
generate an optimized trace that fits inside Dynamics limits
Additionally, for selected plugins, the entire trace can also be persisted into a custom Dataverse table.
The architecture looks like this:
Plugin
│
▼
BufferedTracingService
│
├────────► Custom Log Entity (complete log)
│
▼
Optimized Trace
│
▼
Dynamics Plugin Trace Log
Buffering the Trace
Instead of immediately calling the original tracing service, every message is stored inside an internal collection.
private readonly List<string> _buffer = new();
public void Trace(string format, params object[] args)
{
var message = args?.Length > 0
? string.Format(format, args)
: format;
_buffer.Add(message);
}
Nothing is written immediately.
The plugin continues executing exactly as before.
Keeping the Most Useful Information
Simply cutting the trace after 9 KB wouldn't solve the problem.
We would still lose the final exception or the last executed operations.
Instead, the algorithm keeps:
the beginning
the end
removes only the middle section
The resulting trace becomes:
Plugin started
Reading configuration
Loading configuration
Checking user privileges
...[TRACE TRUNCATED]...
Updating child records
Calling external service
Plugin completed successfully
This is significantly more valuable because it contains both the initial execution context and the final operations.
Persisting the Complete Log
Sometimes even the optimized trace is not enough.
For particularly critical plugins we also wanted the entire execution log.
Instead of storing every plugin trace, only selected plugins are configured for persistence.
private static readonly HashSet<string> _pluginsToLog =
{
"OnPostCreateOnPostUpdateSetManagement"
};
When one of these plugins finishes execution, the complete buffered log is stored inside a custom Dataverse table.
var logRecord = new Entity("dps_log")
{
["dps_description"] = fullLog,
["dps_userid"] = new EntityReference(
"systemuser",
userId)
};
_orgService.Create(logRecord);
This gives us an unlimited execution history while keeping the standard Plugin Trace Log readable.
Integrating with Existing Plugins
One of the nicest aspects of this approach is that existing plugin code doesn't change.
Instead of resolving the standard tracing service:
this.tracingService = new Lazy<ITracingService>(
Get<ITracingService>);
we simply wrap it.
this.tracingService = new Lazy<ITracingService>(() =>
new BufferedTracingService(
Get<ITracingService>(),
OrganizationServiceFactory.CreateOrganizationService(null),
GetType().Name,
Context.PrimaryEntityId));
Every existing call remains exactly the same.
context.TracingService.Trace("Processing Account {0}", account.Id);
No refactoring of business logic is required.
Flushing the Buffer
At the very end of plugin execution, the buffer is flushed.
(context.TracingService as BufferedTracingService)?.Flush();
I usually place this call inside the finally block of my custom PluginBase.
This guarantees that traces are written even when an exception is thrown.
Benefits
Dynamics 365's 10 KB Plugin Trace Log limit is unlikely to disappear anytime soon, but that doesn't mean we have to accept losing valuable diagnostic information.
A buffered tracing service gives us complete control over what gets preserved, making debugging much easier without changing existing plugin code.
In practice, this utility has become part of my standard plugin framework because it solves one of the most common frustrations when troubleshooting long-running Dataverse plugins.
Complete Source Code
Below is the full implementation of the BufferedTracingService used throughout this article.
The following implementation can be copied directly into your plugin framework. Adapt the custom log entity (
dps_log) and fields to match your own Dataverse solution.
using Microsoft.Xrm.Sdk;
using System;
using System.Collections.Generic;
using System.Text;
namespace Avanade
{
public sealed class BufferedTracingService : ITracingService
{
private readonly ITracingService _inner;
private readonly IOrganizationService _orgService;
private readonly string _pluginName;
private readonly Guid _triggerEntityId;
private readonly List<string> _buffer = new List<string>();
private const int MaxD365TraceBytes = 9500;
private const string TruncationMarker = "\r\n...[TRACE TRUNCATED]...\r\n";
public BufferedTracingService(
ITracingService inner,
IOrganizationService orgService,
string pluginName,
Guid triggerEntityId)
{
_inner = inner;
_orgService = orgService;
_pluginName = pluginName;
_triggerEntityId = triggerEntityId;
}
public void Trace(string format, params object[] args)
{
var message = (args != null && args.Length > 0)
? string.Format(format, args)
: format;
_buffer.Add(message);
}
private static readonly HashSet<string> _pluginsToLog = new HashSet<string>
{
" OnPostCreateOnPostUpdateSetMan agement",
};
public void Flush()
{
if (_buffer.Count == 0) return;
if (_pluginsToLog.Contains(_ pluginName))
{
var fullLog = string.Join("\r\n", _buffer);
try
{
var logRecord = new Entity("dps_log")
{
["dps_description"] = fullLog,
["dps_userid"] = new EntityReference("systemuser", new Guid("ba9e89ba-017a-f111-bc81- 00224889f7d8")),
};
_orgService.Create(logRecord);
}
catch { }
}
_inner.Trace(BuildTruncatedTra ce());
}
private string BuildTruncatedTrace()
{
var encoding = Encoding.UTF8;
int headBudget = 3000;
int totalBudget = MaxD365TraceBytes;
int markerBytes = encoding.GetByteCount( TruncationMarker);
var headSb = new StringBuilder();
int headBytes = 0;
int headEndIndex = _buffer.Count;
for (int i = 0; i < _buffer.Count; i++)
{
var line = _buffer[i] + "\r\n";
int lb = encoding.GetByteCount(line);
if (headBytes + lb > headBudget) { headEndIndex = i; break; }
headSb.Append(line);
headBytes += lb;
headEndIndex = i + 1;
}
if (headEndIndex >= _buffer.Count)
return headSb.ToString();
var tailLines = new List<string>();
int tailBytes = 0;
int tailBudget = totalBudget - headBytes - markerBytes;
int tailStartIndex = _buffer.Count;
for (int i = _buffer.Count - 1; i >= headEndIndex; i--)
{
var line = _buffer[i] + "\r\n";
int lb = encoding.GetByteCount(line);
if (tailBytes + lb > tailBudget) break;
tailLines.Insert(0, _buffer[i]);
tailBytes += lb;
tailStartIndex = i;
}
var sb = new StringBuilder();
sb.Append(headSb);
if (tailStartIndex > headEndIndex) sb.Append(TruncationMarker);
foreach (var l in tailLines) sb.AppendLine(l);
return sb.ToString();
}
}
}
PluginContext
this.tracingService = new Lazy<ITracingService>(() =>
new BufferedTracingService(
Get<ITracingService>(),
this.OrganizationServiceFactory.CreateOrganizationService(null),
this.GetType().Name,
this.Context.PrimaryEntityId));
PluginBase
try
{
ExecutePlugin(context);
}
finally
{
(context.TracingService as BufferedTracingService)?.Flush();
}The implementation is intentionally lightweight, requires virtually no changes to existing plugins, and dramatically improves the debugging experience. In production environments where traces frequently exceed the Dynamics 365 limit, this small utility quickly proves its value. It has become a standard component of my Dataverse plugin framework because it preserves the information that matters most when diagnosing complex issues.