Skip to content

Commit d61145d

Browse files
committed
logging restructure
1 parent 99d0fa9 commit d61145d

File tree

9 files changed

+340
-369
lines changed

9 files changed

+340
-369
lines changed

SharedKernel.Demo/Program.cs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using DistributedCache.Extensions;
22
using DistributedCache.Options;
33
using FluentMinimalApiMapper;
4+
using Microsoft.AspNetCore.HttpLogging;
45
using Microsoft.AspNetCore.Mvc;
56
using SharedKernel.Demo2;
67
using ResponseCrafter.Enums;
@@ -39,19 +40,25 @@
3940
.AddOutboundLoggingHandler()
4041
.AddHealthChecks();
4142

43+
builder.Services.ConfigureHttpJsonOptions(options =>
44+
{
45+
options.SerializerOptions.PropertyNamingPolicy = null;
46+
});
4247

4348

4449
builder.Services
4550
.AddHttpClient("RandomApiClient",
4651
client =>
4752
{
53+
client.DefaultRequestHeaders.Add("RequestCustomHeader", "CustomValue");
4854
client.BaseAddress = new Uri("http://localhost");
4955
})
5056
.AddOutboundLoggingHandler();
5157

5258

5359
var app = builder.Build();
5460

61+
5562
app
5663
.UseRequestLogging()
5764
.UseResponseCrafter()
@@ -65,18 +72,53 @@
6572
.MapControllers();
6673

6774

75+
app.MapPost("/receive-file", ([FromForm] IFormFile file) => TypedResults.Ok())
76+
.DisableAntiforgery();
77+
6878
app.MapPost("/params", ([AsParameters] TestTypes testTypes) => TypedResults.Ok(testTypes));
69-
app.MapPost("/body", ([FromBody] TestTypes testTypes) => TypedResults.Ok(testTypes));
70-
app.MapGet("/hello", () => TypedResults.Ok("Hello World!"));
79+
80+
app.MapPost("/body",
81+
([FromBody] TestTypes testTypes, HttpContext httpContext) =>
82+
{
83+
httpContext.Response.ContentType = "application/json";
84+
httpContext.Response.Headers.Append("Custom-Header-Response", "CustomValue");
85+
86+
return TypedResults.Ok(testTypes);
87+
});
7188

7289
app.MapGet("/get-data",
7390
async (IHttpClientFactory httpClientFactory) =>
7491
{
7592
var httpClient = httpClientFactory.CreateClient("RandomApiClient");
7693
httpClient.DefaultRequestHeaders.Add("auth", "hardcoded-auth-value");
77-
var response = await httpClient.GetFromJsonAsync<object>("hello");
7894

79-
return response;
95+
var body = new TestTypes
96+
{
97+
AnimalType = AnimalType.Cat,
98+
JustText = "Hello from Get Data",
99+
JustNumber = 100
100+
};
101+
var content = new StringContent(System.Text.Json.JsonSerializer.Serialize(body),
102+
System.Text.Encoding.UTF8,
103+
"application/json");
104+
105+
var response = await httpClient.PostAsync("body", content);
106+
107+
if (!response.IsSuccessStatusCode)
108+
{
109+
throw new Exception("Something went wrong");
110+
}
111+
112+
var responseBody = await response.Content.ReadAsStringAsync();
113+
114+
var testTypes = System.Text.Json.JsonSerializer.Deserialize<TestTypes>(responseBody);
115+
116+
if (testTypes == null)
117+
{
118+
throw new Exception("Failed to get data from external API");
119+
}
120+
121+
return TypedResults.Ok(testTypes);
80122
});
81123

82124
app.MapHub<MessageHub>("/hub");
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Text.Json;
2+
using Microsoft.AspNetCore.Http;
3+
4+
namespace SharedKernel.Logging.Helpers;
5+
6+
internal static class HttpLogHelper
7+
{
8+
public static async Task<(string Headers, string Body)> CaptureAsync(Stream bodyStream,
9+
IHeaderDictionary headers,
10+
string? mediaType)
11+
{
12+
using var reader = new StreamReader(bodyStream, leaveOpen: true);
13+
var raw = await reader.ReadToEndAsync();
14+
bodyStream.Position = 0;
15+
var hdrs = RedactionHelper.RedactHeaders(headers);
16+
var body = RedactionHelper.RedactBody(mediaType, raw);
17+
return (JsonSerializer.Serialize(hdrs), JsonSerializer.Serialize(body));
18+
}
19+
20+
public static async Task<(string Headers, string Body)> CaptureAsync(Dictionary<string, IEnumerable<string>> headers,
21+
Func<Task<string>> rawReader,
22+
string? mediaType)
23+
{
24+
var hdrs = RedactionHelper.RedactHeaders(headers);
25+
var raw = await rawReader();
26+
var body = RedactionHelper.RedactBody(mediaType, raw);
27+
return (JsonSerializer.Serialize(hdrs), JsonSerializer.Serialize(body));
28+
}
29+
30+
public static Dictionary<string, IEnumerable<string>> CreateHeadersDictionary(HttpRequestMessage req)
31+
{
32+
var dict = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
33+
foreach (var h in req.Headers) dict[h.Key] = h.Value;
34+
if (req.Content?.Headers == null)
35+
{
36+
return dict;
37+
}
38+
39+
{
40+
foreach (var h in req.Content.Headers)
41+
dict[h.Key] = h.Value;
42+
}
43+
return dict;
44+
}
45+
46+
public static Dictionary<string, IEnumerable<string>> CreateHeadersDictionary(HttpResponseMessage res)
47+
{
48+
var dict = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
49+
foreach (var h in res.Headers) dict[h.Key] = h.Value;
50+
{
51+
foreach (var h in res.Content.Headers)
52+
dict[h.Key] = h.Value;
53+
}
54+
return dict;
55+
}
56+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System.Text.Json;
2+
using System.Web;
3+
using Microsoft.AspNetCore.Http;
4+
5+
namespace SharedKernel.Logging.Helpers;
6+
7+
internal static class RedactionHelper
8+
{
9+
private const int MaxPropertyLength = 1024 * 5; // 5 KB
10+
11+
private static readonly HashSet<string> SensitiveKeywords = new(StringComparer.OrdinalIgnoreCase)
12+
{
13+
"pwd",
14+
"pass",
15+
"secret",
16+
"token",
17+
"cookie",
18+
"auth",
19+
"pan",
20+
"cvv",
21+
"cvc",
22+
"cardholder",
23+
"bindingid",
24+
"ssn",
25+
"tin",
26+
"iban",
27+
"swift",
28+
"bankaccount",
29+
"notboundcard"
30+
};
31+
32+
public static Dictionary<string, string> RedactHeaders(IHeaderDictionary headers) =>
33+
headers.ToDictionary(
34+
h => h.Key,
35+
h => SensitiveKeywords.Any(k => h.Key.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0)
36+
? "[REDACTED]"
37+
: h.Value.ToString()!);
38+
39+
public static Dictionary<string, string> RedactHeaders(Dictionary<string, IEnumerable<string>> headers) =>
40+
headers.ToDictionary(
41+
kvp => kvp.Key,
42+
kvp => SensitiveKeywords.Any(k => kvp.Key.Contains(k, StringComparison.OrdinalIgnoreCase))
43+
? "[REDACTED]"
44+
: string.Join(";", kvp.Value));
45+
46+
public static object RedactBody(string? mediaType, string raw)
47+
{
48+
if (string.IsNullOrWhiteSpace(raw))
49+
{
50+
return string.Empty;
51+
}
52+
53+
if (IsJsonMediaType(mediaType))
54+
{
55+
try
56+
{
57+
var el = JsonSerializer.Deserialize<JsonElement>(raw);
58+
return RedactElement(el);
59+
}
60+
catch (JsonException)
61+
{
62+
return "[INVALID_JSON]";
63+
}
64+
}
65+
66+
if (mediaType?.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) == true)
67+
68+
{
69+
var nvc = HttpUtility.ParseQueryString(raw);
70+
var keys = nvc.AllKeys;
71+
72+
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
73+
foreach (var k in keys)
74+
{
75+
if (string.IsNullOrEmpty(k))
76+
continue;
77+
78+
var v = nvc[k] ?? string.Empty;
79+
dict[k] = SensitiveKeywords.Any(s =>
80+
k.Contains(s, StringComparison.OrdinalIgnoreCase) ||
81+
v.Contains(s, StringComparison.OrdinalIgnoreCase))
82+
? "[REDACTED]"
83+
: v;
84+
}
85+
86+
return dict;
87+
}
88+
89+
if (raw.Length <= MaxPropertyLength)
90+
{
91+
return SensitiveKeywords.Any(s => raw.Contains(s, StringComparison.OrdinalIgnoreCase))
92+
? "[REDACTED]"
93+
: raw;
94+
}
95+
96+
var maxKb = MaxPropertyLength / 1024;
97+
var actualKb = raw.Length / 1024;
98+
return $"[OMITTED: max {maxKb}KB, actual {actualKb}KB]";
99+
}
100+
101+
private static bool IsJsonMediaType(string? mediaType) =>
102+
!string.IsNullOrWhiteSpace(mediaType)
103+
&& (mediaType.EndsWith("/json", StringComparison.OrdinalIgnoreCase)
104+
|| mediaType.EndsWith("+json", StringComparison.OrdinalIgnoreCase));
105+
106+
private static object RedactElement(JsonElement el) =>
107+
el.ValueKind switch
108+
{
109+
JsonValueKind.Object => el.EnumerateObject()
110+
.ToDictionary(
111+
p => p.Name,
112+
p => SensitiveKeywords.Any(k =>
113+
p.Name.Contains(k, StringComparison.OrdinalIgnoreCase))
114+
? "[REDACTED]"
115+
: RedactElement(p.Value)),
116+
JsonValueKind.Array => el.EnumerateArray()
117+
.Select(RedactElement)
118+
.ToArray(),
119+
JsonValueKind.String => RedactString(el.GetString()!),
120+
_ => el.GetRawText()
121+
};
122+
123+
private static string RedactString(string value)
124+
{
125+
if (value.Length <= MaxPropertyLength)
126+
{
127+
return SensitiveKeywords.Any(s => value.Contains(s, StringComparison.OrdinalIgnoreCase))
128+
? "[REDACTED]"
129+
: value;
130+
}
131+
132+
const int maxKb = MaxPropertyLength / 1024;
133+
var actualKb = value.Length / 1024;
134+
return $"[OMITTED: max {maxKb}KB, actual {actualKb}KB]";
135+
}
136+
}

0 commit comments

Comments
 (0)