Accigo.Integration.Logging
1.4.0
dotnet add package Accigo.Integration.Logging --version 1.4.0
NuGet\Install-Package Accigo.Integration.Logging -Version 1.4.0
<PackageReference Include="Accigo.Integration.Logging" Version="1.4.0" />
<PackageVersion Include="Accigo.Integration.Logging" Version="1.4.0" />
<PackageReference Include="Accigo.Integration.Logging" />
paket add Accigo.Integration.Logging --version 1.4.0
#r "nuget: Accigo.Integration.Logging, 1.4.0"
#:package Accigo.Integration.Logging@1.4.0
#addin nuget:?package=Accigo.Integration.Logging&version=1.4.0
#tool nuget:?package=Accigo.Integration.Logging&version=1.4.0
Accigo.Integration.Logging
A lightweight .NET logging convention for Azure Functions (isolated worker) integrations. Targets net8.0, net9.0, and net10.0. Emits structured ILogger events and ActivitySource spans so telemetry backends such as Azure Application Insights can correlate runs, steps, and errors across an integration flow.
No companion SDK. No HTTP callbacks. Integrations write structured log entries and spans; your normal telemetry pipeline handles ingestion, querying, and visualization.
Install
Install from NuGet like any other package:
dotnet add package Accigo.Integration.Logging
Requirements:
- .NET 8, .NET 9, or .NET 10
Microsoft.Extensions.Loggingregistered in DI- Optional: Azure Application Insights or another OpenTelemetry-compatible backend if you want centralized telemetry
Register in your Functions host
using Accigo.Integration.Logging.Extensions;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
// Registers the ActivitySource for distributed tracing spans.
services.AddIntegrationLogging();
})
.Build();
host.Run();
Usage
Inject ILogger<T> (standard .NET logging) and use the extension methods:
using Accigo.Integration.Logging.Extensions;
public class OrderSync(ILogger<OrderSync> logger)
{
[Function("ProcessOrder")]
public async Task Run([ServiceBusTrigger("orders")] ServiceBusReceivedMessage msg)
{
// BeginIntegrationRun emits IntegrationRunStarted for a new run (no runId
// supplied). If runId is an existing run's id, the call is treated as a
// continuation and emits a "resume" step instead — see "Emitted events" below.
// Dispose emits IntegrationRunCompleted (suppressed if LogIntegrationRunFailed was called).
// Seed runId from CorrelationId (the producing run id, shared by every
// message of a run), NOT MessageId — MessageId is unique per message.
// See "Correlating runs and Service Bus messages" below.
using var run = logger.BeginIntegrationRun(
integrationId: "order-sync",
runId: msg.CorrelationId ?? msg.MessageId,
triggerSource: "servicebus:orders",
isNewRun: false,
companyId: "company-123");
try
{
// Log step information — timing comes from Application Insights timestamps
logger.LogIntegrationStepInformation(run, "validate", "Validating payload");
ValidatePayload(msg.Body);
logger.LogIntegrationStepInformation(run, "validate", "Validation succeeded");
logger.LogIntegrationStepInformation(run, "transform", "Transforming data");
var transformed = TransformData(msg.Body);
logger.LogIntegrationStepInformation(run, "transform", $"Transformed {transformed.Count} records");
logger.LogIntegrationStepInformation(run, "load", "Writing to ERP");
await WriteToErp(transformed);
logger.LogIntegrationStepInformation(run, "load", "Successfully wrote all records");
}
catch (Exception ex)
{
// Marks the run failed so Dispose does not emit RunCompleted.
logger.LogIntegrationRunFailed(run, ex, errorCode: "ERR_UNEXPECTED");
throw;
}
}
}
Seed runId from the Service Bus CorrelationId (the producing run id) so
every message of a run correlates to one run and the Hub can find and resend an
individual dead-lettered message. See
Correlating runs and Service Bus messages.
Pass companyId when the run belongs to a known company. Use the same stable
identifier on every continuation of that run; it is descriptive run metadata,
not a replacement for integration access or customer tenancy. It may be omitted
for existing callers and runs without a company identifier. Surrounding Unicode
whitespace is trimmed before emission, values that become blank are treated as
absent, and the canonical value must be at most 128 characters. To preserve
binary compatibility with prebuilt callers, company-tagged calls also pass runId,
triggerSource, and isNewRun explicitly; use null for unused strings.
Starting with package version 1.4.0, if the company is only discovered after the run starts, pass it with any subsequent step log:
using var run = logger.BeginIntegrationRun("order-sync");
// ... companyId becomes available during processing ...
logger.LogIntegrationStepInformation(run, "load", "Writing to ERP", companyId);
The step key and message describe the work; neither is interpreted to identify
the company. Only the supplied companyId binds the run. The step event
immediately carries the company ID, so the Hub associates it
with the existing run when telemetry arrives, without restarting it or adding
an extra event. Later step, failure, and completion events and the run's span
carry the ID too; earlier telemetry cannot be changed retroactively. Supplying
the same normalized ID on later steps is allowed. Blank or overlong IDs, a
different ID on the same run, or assigning one after disposal are rejected.
Correlating runs and Service Bus messages
A single integration run often fetches a batch and sends N messages — one run, many messages. When one of those messages fails downstream and dead-letters, an operator must find and resend that one message without disturbing its siblings. That only works if runs and messages are identified separately:
| Broker field | Meaning | Cardinality |
|---|---|---|
CorrelationId |
The run id (run.RunId) |
Shared by every message of the run |
MessageId |
The message id (the resend handle) | Unique per message |
Producer — set both when you send each message:
using var run = logger.BeginIntegrationRun("order-sync", runId: Guid.NewGuid().ToString());
foreach (var order in await FetchOrdersAsync())
{
var message = new ServiceBusMessage(Serialize(order))
{
// Unique per message — the handle used to resend exactly this one.
MessageId = Guid.NewGuid().ToString(),
// Shared across the run — groups the batch under run.RunId.
CorrelationId = run.RunId,
};
await sender.SendMessageAsync(message);
}
Consumer — seed the run from CorrelationId, not MessageId:
runId: msg.CorrelationId ?? msg.MessageId // fall back only if the producer set no CorrelationId
Why not runId = MessageId? With a unique MessageId per message, seeding the
run from it makes every message look like its own run, so the N siblings of a batch
no longer group together. Conversely, reusing one id for both MessageId and
CorrelationId collapses the batch into a single dead-letter row (the Hub dedups on
(integrationId, messageId)), so you can no longer resend an individual message.
Keep them distinct.
Fan-out to independent child runs
The batch pattern above assumes each dispatched message is a sibling of the same run — fine when the messages are fungible units of one logical operation. It does not apply when a consumer receives a batch message and then itself dispatches further, independent units of work that each deserve their own pass/fail outcome (e.g. a trigger fans out N files, and each file's downstream import is its own customer-visible success or failure). Reusing the parent run's id all the way down that chain merges every child's status onto one run — a later child's success can silently overwrite an earlier child's failure.
For that shape, mint a new id per child and pass isNewRun: true so the child
still gets its own IntegrationRunStarted (not a resume step), while the id is
still known up front for correlating the outgoing message:
// Parent — dispatches N independent children, each with its own new run id.
foreach (var file in files)
{
var childRunId = Guid.NewGuid().ToString();
var message = new ServiceBusMessage(Serialize(file))
{
MessageId = Guid.NewGuid().ToString(),
CorrelationId = childRunId, // the CHILD's run id, not the parent's
};
await sender.SendMessageAsync(message);
}
// Child — starts its OWN run using the id the parent chose.
using var run = logger.BeginIntegrationRun(
"order-sync", runId: msg.CorrelationId, isNewRun: true);
Rule of thumb: reuse the parent's run.RunId (plain continuation) only when the
dispatched messages are siblings of one run. Mint a fresh id + isNewRun: true
whenever each dispatched message is its own independent unit of work.
Step information
Log free-form messages correlated to a step so your observability tooling can show what a step did. Timing comes from Application Insights timestamps — no need for explicit start/complete events:
logger.LogIntegrationStepInformation(run, "fetch", "Fetching orders from SAP");
// ... do the work ...
logger.LogIntegrationStepInformation(run, "fetch", "Fetched 142 orders from batch 7892");
logger.LogIntegrationStepInformation(run, "fetch", "Skipped 3 orders with missing customer ID");
Each call to LogIntegrationStepInformation emits one IntegrationStepInformation log event. Callers can call it multiple times per step.
Rules:
messagemust not be null or whitespace —ArgumentExceptionis thrown.- Messages longer than 1024 characters are silently truncated.
Do not put PII or credentials in step messages. This data flows to your telemetry backend and any downstream observability store that ingests these events.
Emitted events
All events are emitted via ILogger.LogInformation / ILogger.LogError as structured log entries with named properties.
When using Azure Application Insights, these events land in the traces table and the dimensions below appear in customDimensions.
| Event | When | Extra dimensions |
|---|---|---|
IntegrationRunStarted |
BeginIntegrationRun() with no runId (new run) |
integrationId, runId, triggerSource (optional), companyId (optional) |
IntegrationRunCompanyIdentified |
run.SetCompanyId() once a company becomes known |
integrationId, runId, companyId |
IntegrationStepInformation |
LogIntegrationStepInformation(), or BeginIntegrationRun() with an existing runId (stepKey resume) |
integrationId, runId, stepKey, stepMessage, companyId (optional) |
IntegrationRunCompleted |
Disposing a run context without failure | integrationId, runId, durationMs, companyId (optional) |
IntegrationRunFailed |
LogIntegrationRunFailed() |
integrationId, runId, errorCode, errorMessage, severity, durationMs, exceptionType?, stackTrace?, companyId (optional) |
Passing an existing run's runId to BeginIntegrationRun() (e.g. a trigger→queue→import split sharing one runId across function invocations) treats the call as a continuation: it emits the resume step instead of a second IntegrationRunStarted, so the run timeline doesn't show a spurious restart. Pass isNewRun: true to override this and force a fresh IntegrationRunStarted even with a supplied runId — see Fan-out to independent child runs.
Deprecated events (backward compatibility only):
IntegrationStepStarted— useIntegrationStepInformationinstead; timing comes from Application Insights timestamps. Both includecompanyIdwhen supplied.IntegrationStepCompleted— useIntegrationStepInformationinstead; timing comes from Application Insights timestamps. Both includecompanyIdwhen supplied.
In addition to log entries, each run starts an ActivitySource span (Integration.Run.*) for OpenTelemetry-compatible collectors.
Consuming the emitted events
Any telemetry pipeline that preserves structured log properties can build dashboards, alerts, or ingestion jobs on top of these events. If you use Azure Application Insights, query traces by the event names and dimensions documented above.
Versioning
The package follows SemVer. Breaking changes to event names or required dimensions will bump the major version. Deprecated step lifecycle APIs remain for backward compatibility and will be removed in v2.0.0.
NuGet package versions are immutable. Before publishing, bump <Version> in the project and publish the matching tag (tools/logging/vX.Y.Z), or dispatch the publish workflow from the commit containing that version. The workflow stops if the package version already exists, so a symbols package from a different build cannot be uploaded against it.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
-
net8.0
-
net9.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.