Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<PackageVersion Include="Asp.Versioning.OpenApi" Version="10.2.3" />
<PackageVersion Include="coverlet.MTP" Version="10.1.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.13" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.13" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.13" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.13" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.13" />
Expand All @@ -30,6 +31,7 @@
<PackageVersion Include="Polly" Version="8.8.0" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.13" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.13" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.13" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="System.Text.Json" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.13" />
Expand Down
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>
57 changes: 56 additions & 1 deletion Shared.Logger/ILogger.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace Shared.Logger;

using System;
using System.Globalization;
using System.Text;

/// <summary>
///
Expand All @@ -20,22 +22,75 @@

void LogCritical(Exception exception);

void LogCritical(String messageTemplate, // NOSONAR - delegates to the required string-only overload, so it is not recursive.

Check failure on line 25 in Shared.Logger/ILogger.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Shared.Logger/ILogger.cs#L25

Add a way to break out of this method's recursion.
params Object?[] args) => this.LogCritical(FormatMessageTemplate(messageTemplate, args));

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(Convert.ToString(args[argumentIndex++], CultureInfo.InvariantCulture));
index = end + 1;
continue;
}
}
result.Append(messageTemplate[index++]);
}
return result.ToString();
}

}
Loading
Loading