Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Shared.Logger.Sample/NLog.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
throwConfigExceptions="true">
<targets>
<target xsi:type="File"
name="structuredJsonFile"
fileName="${basedir}/logs/${whenEmpty:inner=${scopeproperty:item=tenantId}:whenEmpty=}/structured-logger-${shortdate}.json"
keepFileOpen="false">
<layout xsi:type="JsonLayout"
includeEventProperties="true"
excludeProperties="timestampUtc">
<attribute name="timestampUtc" layout="${date:universalTime=true:format=o}" />
<attribute name="level" layout="${level:uppercase=true}" />
<attribute name="message" layout="${message}" />
<attribute name="exception" layout="${exception:format=tostring}" />
</layout>
</target>
</targets>

<rules>
<logger name="*" minlevel="Trace" writeTo="structuredJsonFile" />
</rules>
</nlog>
63 changes: 63 additions & 0 deletions Shared.Logger.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using NLog;
using Shared.Logger.TennantContext;
using StaticLogger = Shared.Logger.Logger;

Boolean generateHighVolumeLogs = true;
const Int32 HighVolumeLogEventCount = 1000;

LogManager.Setup().LoadConfigurationFromFile("NLog.config");

NLog.Logger nlogLogger = LogManager.GetLogger("Shared.Logger.Sample");
StaticLogger.Initialise(nlogLogger, "shared-logger-sample");

// Simulate the tenant context normally populated by the service middleware.
TenantContext tenantContext = new();
tenantContext.Initialise(
new TenantIdentifiers(
EstateId: Guid.Parse("1e543b2a-d100-4ec1-9735-8590aab2c819"),
MerchantId: Guid.Parse("579267d9-46a6-4057-8c66-0b34d7494a28")),
perTenantLogsEnabled: false);
tenantContext.SetCorrelationId(CorrelationId.From(Guid.Parse("b0354c7e-b6de-4a27-9a54-715dc3e56566")));
TenantContext.CurrentTenant = tenantContext;

Guid transactionId = Guid.NewGuid();
StaticLogger.LogTrace("Starting sample run {runId}", Guid.NewGuid());
StaticLogger.LogDebug("Loaded {recordCount} records for {tenantName}", 12, "Example Estate");
StaticLogger.LogInformation("Processed transaction {transactionId} for {amount} {currency}", transactionId, 125.50m, "GBP");
StaticLogger.LogWarning("Transaction {transactionId} required a retry after {attemptCount} attempts", transactionId, 2);

try
{
throw new InvalidOperationException("Example failure for the structured exception log.");
}
catch (InvalidOperationException exception)
{
StaticLogger.LogError(exception, "Could not process transaction {transactionId}", transactionId);
}

StaticLogger.LogCritical("Sample critical event for {component}", "payment processor");

if (generateHighVolumeLogs)
{
for (Int32 eventNumber = 1; eventNumber <= HighVolumeLogEventCount; eventNumber++)
{
TenantContext volumeTenantContext = new();
volumeTenantContext.Initialise(
new TenantIdentifiers(EstateId: Guid.NewGuid(), MerchantId: Guid.NewGuid()),
perTenantLogsEnabled: true);
volumeTenantContext.SetCorrelationId(CorrelationId.From(Guid.NewGuid()));
TenantContext.CurrentTenant = volumeTenantContext;

StaticLogger.LogInformation(
"Volume sample event {eventNumber} of {totalEvents} for transaction {transactionId} with amount {amount}",
eventNumber,
HighVolumeLogEventCount,
Guid.NewGuid(),
(eventNumber % 500) + 0.25m);
}
}

TenantContext.CurrentTenant = new TenantContext();
LogManager.Shutdown();

Console.WriteLine($"Structured log entries written under: {Path.Combine(AppContext.BaseDirectory, "logs")}");
13 changes: 13 additions & 0 deletions Shared.Logger.Sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Shared.Logger sample

This console app demonstrates structured message-template calls through `Shared.Logger` and NLog. NLog reads [NLog.config](NLog.config) and writes JSON lines with event properties included, so template values and tenant/correlation metadata are available as separate fields. The tenant scope controls file routing: the tenant-scoped event goes into its tenant-specific subfolder, while the regular event goes into `logs/shared`.

Run it from the repository root:

```powershell
dotnet run --project Shared.Logger.Sample
```

The sample writes under `logs` in the app's output directory. With per-tenant logging enabled, each logger call produces the regular event and the tenant-scoped event, which this config routes separately.

To generate additional volume, set `generateHighVolumeLogs` to `true` near the top of `Program.cs`. It then writes 1,000 more structured events, each with a fresh correlation ID, estate ID, and merchant ID (and each routed to the regular and tenant-specific files); change `HighVolumeLogEventCount` to adjust the batch size.
17 changes: 17 additions & 0 deletions Shared.Logger.Sample/Shared.Logger.Sample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="NLog" />
<ProjectReference Include="..\Shared.Logger\Shared.Logger.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="NLog.config" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
81 changes: 80 additions & 1 deletion Shared.Logger/ILogger.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
namespace Shared.Logger;

using System;
using System.Collections;
using System.Globalization;
using System.Text;
using System.Text.Json;

/// <summary>
///
Expand All @@ -20,22 +24,97 @@ public interface ILogger

void LogCritical(Exception exception);

#pragma warning disable S2190 // This overload delegates to the required string-only overload; it does not call itself.
void LogCritical(String messageTemplate,
params Object?[] args) => this.LogCritical(FormatMessageTemplate(messageTemplate, args));
#pragma warning restore S2190

void LogCritical(Exception exception,
String messageTemplate,
params Object?[] args) => this.LogCritical(FormatMessageTemplate(messageTemplate, args), exception);

void LogCritical(String message,
Exception exception);

void LogDebug(String message);

void LogDebug(String messageTemplate,
params Object?[] args) => this.LogDebug(FormatMessageTemplate(messageTemplate, args));

void LogError(Exception exception);

void LogError(String messageTemplate,
params Object?[] args) => this.LogError(FormatMessageTemplate(messageTemplate, args));

void LogError(Exception exception,
String messageTemplate,
params Object?[] args) => this.LogError(FormatMessageTemplate(messageTemplate, args), exception);

void LogError(String message);

void LogError(String message,
Exception exception);

void LogInformation(String message);

void LogInformation(String messageTemplate,
params Object?[] args) => this.LogInformation(FormatMessageTemplate(messageTemplate, args));

void LogTrace(String message);

void LogTrace(String messageTemplate,
params Object?[] args) => this.LogTrace(FormatMessageTemplate(messageTemplate, args));

void LogWarning(String message);

}
void LogWarning(String messageTemplate,
params Object?[] args) => this.LogWarning(FormatMessageTemplate(messageTemplate, args));

private static String FormatMessageTemplate(String messageTemplate, Object?[] args)
{
if (args == null || args.Length == 0) return messageTemplate;
StringBuilder result = new(messageTemplate.Length + args.Length * 8);
Int32 argumentIndex = 0;
Int32 index = 0;
while (index < messageTemplate.Length)
{
if (messageTemplate[index] == '{' && index + 1 < messageTemplate.Length && messageTemplate[index + 1] == '{')
{
result.Append("{{"); index += 2; continue;
}
if (messageTemplate[index] == '{')
{
Int32 end = messageTemplate.IndexOf('}', index + 1);
if (end >= 0 && argumentIndex < args.Length)
{
result.Append(FormatArgument(args[argumentIndex++]));
index = end + 1;
continue;
}
}
result.Append(messageTemplate[index++]);
}
return result.ToString();
}

private static String FormatArgument(Object? argument)
{
if (argument is IEnumerable && argument is not String)
{
try
{
return JsonSerializer.Serialize(argument, argument.GetType());
}
catch (NotSupportedException)
{
// Unsupported collection types use the invariant scalar fallback below.
}
catch (JsonException)
{
// Collections that cannot be serialized also use the invariant scalar fallback below.
}
}
return Convert.ToString(argument, CultureInfo.InvariantCulture) ?? String.Empty;
}

}
Loading
Loading