diff --git a/Compendium.sln b/Compendium.sln index 64dc57a..873e95e 100644 --- a/Compendium.sln +++ b/Compendium.sln @@ -95,6 +95,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Architecture", "Architectur EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.ArchitectureTests", "tests\Architecture\Compendium.ArchitectureTests\Compendium.ArchitectureTests.csproj", "{D685E8D8-B3DC-4A65-9ED3-675776610190}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.Shared", "src\Adapters\Compendium.Adapters.Shared\Compendium.Adapters.Shared.csproj", "{DE527E82-7509-4E3F-B002-8D53CFAA97DA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -232,6 +234,10 @@ Global {D685E8D8-B3DC-4A65-9ED3-675776610190}.Debug|Any CPU.Build.0 = Debug|Any CPU {D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.ActiveCfg = Release|Any CPU {D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.Build.0 = Release|Any CPU + {DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {FE421F00-7FFD-4666-A961-F1FF325ECD34} = {E35C8F52-5000-4427-9589-AEB5987C1AC6} @@ -278,5 +284,6 @@ Global {C4495E66-500F-4582-B56D-C30ED0F41FF3} = {A0005488-4247-4872-9CD5-95696E47BEA5} {72B1C880-12A5-4568-85E0-3800536158DC} = {B23A5693-C266-43DC-8D3E-CBB108131762} {D685E8D8-B3DC-4A65-9ED3-675776610190} = {72B1C880-12A5-4568-85E0-3800536158DC} + {DE527E82-7509-4E3F-B002-8D53CFAA97DA} = {73261E87-8FCA-40B6-940B-E25CBDBE33FB} EndGlobalSection EndGlobal diff --git a/src/Adapters/Compendium.Adapters.LemonSqueezy/Http/LemonSqueezyHttpClient.cs b/src/Adapters/Compendium.Adapters.LemonSqueezy/Http/LemonSqueezyHttpClient.cs index 8d5255d..ff91629 100644 --- a/src/Adapters/Compendium.Adapters.LemonSqueezy/Http/LemonSqueezyHttpClient.cs +++ b/src/Adapters/Compendium.Adapters.LemonSqueezy/Http/LemonSqueezyHttpClient.cs @@ -282,7 +282,7 @@ private async Task>> GetResourceAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in LemonSqueezy GetResourceAsync"); return Error.Failure("LemonSqueezy.HttpError", ex.Message); } } @@ -307,7 +307,7 @@ private async Task>>> GetCollectionAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in LemonSqueezy GetCollectionAsync"); return Error.Failure("LemonSqueezy.HttpError", ex.Message); } } @@ -343,7 +343,7 @@ private async Task>> PostResourceAsync>> PatchResourceAsync DeleteAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in LemonSqueezy DeleteAsync"); return Error.Failure("LemonSqueezy.HttpError", ex.Message); } } @@ -432,7 +432,7 @@ private async Task> PostLicenseApiAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling LemonSqueezy License API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in LemonSqueezy License PostAsync"); return Error.Failure("LemonSqueezy.HttpError", ex.Message); } } diff --git a/src/Adapters/Compendium.Adapters.LemonSqueezy/Services/LemonSqueezyBillingService.cs b/src/Adapters/Compendium.Adapters.LemonSqueezy/Services/LemonSqueezyBillingService.cs index fa3f7f0..e890279 100644 --- a/src/Adapters/Compendium.Adapters.LemonSqueezy/Services/LemonSqueezyBillingService.cs +++ b/src/Adapters/Compendium.Adapters.LemonSqueezy/Services/LemonSqueezyBillingService.cs @@ -5,6 +5,7 @@ // // ----------------------------------------------------------------------- +using System.Diagnostics; using Compendium.Adapters.LemonSqueezy.Configuration; using Compendium.Adapters.LemonSqueezy.Http; using Compendium.Adapters.LemonSqueezy.Http.Models; @@ -130,7 +131,7 @@ public async Task> GetCustomerByEmailAsync( { ArgumentNullException.ThrowIfNull(email); - _logger.LogDebug("Getting customer by email {Email}", email); + _logger.LogDebug("Getting customer by email (activity {ActivityId})", Activity.Current?.Id); var result = await _httpClient.ListCustomersAsync(email, cancellationToken); diff --git a/src/Adapters/Compendium.Adapters.Listmonk/Http/ListmonkHttpClient.cs b/src/Adapters/Compendium.Adapters.Listmonk/Http/ListmonkHttpClient.cs index 496481e..d0f0476 100644 --- a/src/Adapters/Compendium.Adapters.Listmonk/Http/ListmonkHttpClient.cs +++ b/src/Adapters/Compendium.Adapters.Listmonk/Http/ListmonkHttpClient.cs @@ -341,7 +341,7 @@ private async Task> GetAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in Listmonk GetAsync"); return Error.Failure("Listmonk.HttpError", ex.Message); } } @@ -366,7 +366,7 @@ private async Task>> GetListAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in Listmonk GetListAsync"); return Error.Failure("Listmonk.HttpError", ex.Message); } } @@ -391,7 +391,7 @@ private async Task>> GetPaginatedAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in Listmonk GetPaginatedAsync"); return Error.Failure("Listmonk.HttpError", ex.Message); } } @@ -423,7 +423,7 @@ private async Task> PostAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in Listmonk PostAsync"); return Error.Failure("Listmonk.HttpError", ex.Message); } } @@ -455,7 +455,7 @@ private async Task> PutAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in Listmonk PutAsync"); return Error.Failure("Listmonk.HttpError", ex.Message); } } @@ -479,7 +479,7 @@ private async Task PutAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in Listmonk PutAsync (no response)"); return Error.Failure("Listmonk.HttpError", ex.Message); } } @@ -501,7 +501,7 @@ private async Task DeleteAsync( } catch (HttpRequestException ex) { - _logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint); + _logger.LogError(ex, "HTTP error in Listmonk DeleteAsync"); return Error.Failure("Listmonk.HttpError", ex.Message); } } diff --git a/src/Adapters/Compendium.Adapters.Listmonk/Services/ListmonkNewsletterService.cs b/src/Adapters/Compendium.Adapters.Listmonk/Services/ListmonkNewsletterService.cs index 41dfeff..21553d1 100644 --- a/src/Adapters/Compendium.Adapters.Listmonk/Services/ListmonkNewsletterService.cs +++ b/src/Adapters/Compendium.Adapters.Listmonk/Services/ListmonkNewsletterService.cs @@ -5,6 +5,7 @@ // // ----------------------------------------------------------------------- +using System.Diagnostics; using Compendium.Adapters.Listmonk.Configuration; using Compendium.Adapters.Listmonk.Http; using Compendium.Adapters.Listmonk.Http.Models; @@ -40,7 +41,7 @@ public async Task> SubscribeAsync( { ArgumentNullException.ThrowIfNull(request); - _logger.LogInformation("Subscribing {Email} to newsletter", request.Email); + _logger.LogInformation("Subscribing to newsletter (activity {ActivityId})", Activity.Current?.Id); var listIds = new List(); @@ -74,12 +75,12 @@ public async Task> SubscribeAsync( if (result.IsFailure) { - _logger.LogWarning("Failed to subscribe {Email}: {Error}", request.Email, result.Error.Message); + _logger.LogWarning("Failed to subscribe (activity {ActivityId}): {Error}", Activity.Current?.Id, result.Error.Message); return result.Error; } var subscriber = MapToSubscriber(result.Value); - _logger.LogInformation("Subscribed {Email} with ID {SubscriberId}", request.Email, subscriber.Id); + _logger.LogInformation("Subscribed with ID {SubscriberId}", subscriber.Id); return subscriber; } @@ -92,7 +93,7 @@ public async Task UnsubscribeAsync( { ArgumentNullException.ThrowIfNull(email); - _logger.LogInformation("Unsubscribing {Email} from list {ListId}", email, listId ?? "all"); + _logger.LogInformation("Unsubscribing from list {ListId} (activity {ActivityId})", listId ?? "all", Activity.Current?.Id); // First, find the subscriber by email var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken); @@ -111,12 +112,12 @@ public async Task UnsubscribeAsync( if (result.IsFailure) { - _logger.LogWarning("Failed to unsubscribe {Email} from list {ListId}: {Error}", - email, listId, result.Error.Message); + _logger.LogWarning("Failed to unsubscribe {SubscriberId} from list {ListId}: {Error}", + subscriberId, listId, result.Error.Message); } else { - _logger.LogInformation("Unsubscribed {Email} from list {ListId}", email, listId); + _logger.LogInformation("Unsubscribed {SubscriberId} from list {ListId}", subscriberId, listId); } return result; @@ -133,11 +134,11 @@ public async Task UnsubscribeAsync( if (result.IsFailure) { - _logger.LogWarning("Failed to unsubscribe {Email}: {Error}", email, result.Error.Message); + _logger.LogWarning("Failed to unsubscribe {SubscriberId}: {Error}", subscriberId, result.Error.Message); } else { - _logger.LogInformation("Unsubscribed {Email} from all lists", email); + _logger.LogInformation("Unsubscribed {SubscriberId} from all lists", subscriberId); } return result.IsSuccess ? Result.Success() : result.Error; @@ -151,7 +152,7 @@ public async Task> GetSubscriberAsync( { ArgumentNullException.ThrowIfNull(email); - _logger.LogDebug("Getting subscriber by email {Email}", email); + _logger.LogDebug("Getting subscriber by email (activity {ActivityId})", Activity.Current?.Id); var result = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken); @@ -172,7 +173,7 @@ public async Task UpdateSubscriberAttributesAsync( ArgumentNullException.ThrowIfNull(email); ArgumentNullException.ThrowIfNull(attributes); - _logger.LogInformation("Updating attributes for subscriber {Email}", email); + _logger.LogInformation("Updating subscriber attributes (activity {ActivityId})", Activity.Current?.Id); // First, find the subscriber by email var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken); @@ -191,12 +192,12 @@ public async Task UpdateSubscriberAttributesAsync( if (result.IsFailure) { - _logger.LogWarning("Failed to update attributes for {Email}: {Error}", - email, result.Error.Message); + _logger.LogWarning("Failed to update attributes for {SubscriberId}: {Error}", + subscriberResult.Value.Id, result.Error.Message); } else { - _logger.LogInformation("Updated attributes for subscriber {Email}", email); + _logger.LogInformation("Updated attributes for subscriber {SubscriberId}", subscriberResult.Value.Id); } return result.IsSuccess ? Result.Success() : result.Error; diff --git a/src/Adapters/Compendium.Adapters.Shared/Compendium.Adapters.Shared.csproj b/src/Adapters/Compendium.Adapters.Shared/Compendium.Adapters.Shared.csproj new file mode 100644 index 0000000..349b0a7 --- /dev/null +++ b/src/Adapters/Compendium.Adapters.Shared/Compendium.Adapters.Shared.csproj @@ -0,0 +1,10 @@ + + + + Compendium.Adapters.Shared + Compendium.Adapters.Shared + Compendium.Adapters.Shared + Shared utilities for Compendium adapters: PII masking helpers, logging conventions. + + + diff --git a/src/Adapters/Compendium.Adapters.Shared/Logging/PiiMasking.cs b/src/Adapters/Compendium.Adapters.Shared/Logging/PiiMasking.cs new file mode 100644 index 0000000..b3dc1cb --- /dev/null +++ b/src/Adapters/Compendium.Adapters.Shared/Logging/PiiMasking.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License. +// See LICENSE in the project root for license information. +// +// ----------------------------------------------------------------------- + +namespace Compendium.Adapters.Shared.Logging; + +/// +/// PII masking helpers for log statements. Use sparingly — prefer non-PII identifiers +/// (subscriber_id, customer_id, activity_id) over masked PII per GDPR data-minimization. +/// +public static class PiiMasking +{ + /// + /// Masks an email for logging: "john.doe@acme.com" → "j***@acme.com". + /// Returns "<empty>" or "<null>" for non-values. + /// Use only when subscriber_id/customer_id is unavailable AND email correlation is required for debugging. + /// + /// The email to mask. + /// Masked email, or a placeholder for empty/invalid input. + public static string MaskEmail(string? email) + { + if (string.IsNullOrWhiteSpace(email)) return ""; + var atIndex = email.IndexOf('@'); + if (atIndex <= 0) return "***"; + return $"{email[0]}***{email[atIndex..]}"; + } +} diff --git a/src/Adapters/Compendium.Adapters.Stripe/Services/StripeBillingService.cs b/src/Adapters/Compendium.Adapters.Stripe/Services/StripeBillingService.cs index 0701aa8..ed1102a 100644 --- a/src/Adapters/Compendium.Adapters.Stripe/Services/StripeBillingService.cs +++ b/src/Adapters/Compendium.Adapters.Stripe/Services/StripeBillingService.cs @@ -5,6 +5,7 @@ // // ----------------------------------------------------------------------- +using System.Diagnostics; using Compendium.Adapters.Stripe.Configuration; using Stripe.Checkout; @@ -131,7 +132,7 @@ public async Task> GetCustomerByEmailAsync( } catch (StripeException ex) { - _logger.LogError(ex, "Stripe customer lookup by email failed for {Email}", email); + _logger.LogError(ex, "Stripe customer lookup by email failed (activity {ActivityId})", Activity.Current?.Id); return Result.Failure( Error.Failure("Billing.Stripe.GetCustomerByEmailFailed", ex.Message)); } @@ -184,7 +185,7 @@ public async Task> UpsertCustomerAsync( } catch (StripeException ex) { - _logger.LogError(ex, "Stripe customer upsert failed for {Email}", request.Email); + _logger.LogError(ex, "Stripe customer upsert failed (activity {ActivityId})", Activity.Current?.Id); return Result.Failure( Error.Failure("Billing.Stripe.UpsertCustomerFailed", ex.Message)); } diff --git a/src/Adapters/Compendium.Adapters.Zitadel/Services/ZitadelUserService.cs b/src/Adapters/Compendium.Adapters.Zitadel/Services/ZitadelUserService.cs index 79d6987..08f2fa2 100644 --- a/src/Adapters/Compendium.Adapters.Zitadel/Services/ZitadelUserService.cs +++ b/src/Adapters/Compendium.Adapters.Zitadel/Services/ZitadelUserService.cs @@ -136,7 +136,7 @@ public async Task> GetUserByEmailAsync( // POM-170: debug logs also flow into centralised log stores, so use the // short hash here as well. - _logger.LogDebug("Getting user by email (hash {EmailHashPrefix})", HashPrefix(email)); + _logger.LogDebug("Getting user by email (hash {HashPrefix})", HashPrefix(email)); var searchRequest = new ZitadelUserSearchRequest {