Skip to content

Commit f9b6f45

Browse files
Add feature to extract extra values from the request and store in keys automatically
1 parent e601109 commit f9b6f45

File tree

4 files changed

+83
-15
lines changed

4 files changed

+83
-15
lines changed

libraries/src/AWS.Lambda.Powertools.Logging/Internal/LoggingAspect.cs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System;
17+
using System.Collections.Generic;
1718
using System.IO;
1819
using System.Linq;
1920
using System.Reflection;
@@ -53,6 +54,11 @@ public class LoggingAspect
5354
/// The correlation identifier path
5455
/// </summary>
5556
private string _correlationIdPath;
57+
58+
/// <summary>
59+
/// Paths to arbitrary values to extract from the input object and the names of the keys to store them in
60+
/// </summary>
61+
private IEnumerable<(string Path, string KeyName)> _extractedKeyPaths;
5662

5763
/// <summary>
5864
/// The Powertools for AWS Lambda (.NET) configurations
@@ -136,6 +142,7 @@ public void OnEntry(
136142

137143
var logEvent = trigger.LogEvent;
138144
_correlationIdPath = trigger.CorrelationIdPath;
145+
_extractedKeyPaths = trigger.ExtractedKeyPaths;
139146
_clearState = trigger.ClearState;
140147

141148
Logger.LoggerProvider = new LoggerProvider(_config, _powertoolsConfigurations, _systemWrapper);
@@ -153,6 +160,7 @@ public void OnEntry(
153160
CaptureXrayTraceId();
154161
CaptureLambdaContext(eventArgs);
155162
CaptureCorrelationId(eventObject);
163+
CaptureExtractedKeyPaths(eventObject);
156164
if (logEvent || _powertoolsConfigurations.LoggerLogEvent)
157165
LogEvent(eventObject);
158166
}
@@ -224,48 +232,73 @@ private void CaptureLambdaContext(AspectEventArgs eventArgs)
224232
/// <param name="eventArg">The event argument.</param>
225233
private void CaptureCorrelationId(object eventArg)
226234
{
227-
if (string.IsNullOrWhiteSpace(_correlationIdPath))
235+
CaptureKeyFromPath(eventArg, _correlationIdPath, LoggingConstants.KeyCorrelationId);
236+
}
237+
238+
/// <summary>
239+
/// Captures the extracted key paths.
240+
/// </summary>
241+
/// <param name="eventArg">The event argument.</param>
242+
private void CaptureExtractedKeyPaths(object eventArg)
243+
{
244+
if (!_extractedKeyPaths?.Any() ?? true)
245+
return;
246+
foreach (var (path, keyName) in _extractedKeyPaths)
247+
{
248+
CaptureKeyFromPath(eventArg, path, keyName);
249+
}
250+
}
251+
252+
/// <summary>
253+
/// Captures an arbitrary value from the path and stores it in a key
254+
/// </summary>
255+
/// <param name="eventArg">The event argument.</param>
256+
/// <param name="path">The path to the input data.</param>
257+
/// <param name="keyName">The name of the key to store the data in.</param>
258+
private void CaptureKeyFromPath(object eventArg, string path, string keyName)
259+
{
260+
if (string.IsNullOrWhiteSpace(path) || string.IsNullOrWhiteSpace(keyName))
228261
return;
229262

230-
var correlationIdPaths = _correlationIdPath
263+
var paths = path
231264
.Split(CorrelationIdPaths.Separator, StringSplitOptions.RemoveEmptyEntries);
232265

233-
if (!correlationIdPaths.Any())
266+
if (!paths.Any())
234267
return;
235268

236269
if (eventArg is null)
237270
{
238271
if (IsDebug())
239272
_systemWrapper.LogLine(
240-
"Skipping CorrelationId capture because event parameter not found.");
273+
$"Skipping {keyName} capture because event parameter not found.");
241274
return;
242275
}
243276

244277
try
245278
{
246-
var correlationId = string.Empty;
279+
var keyValue = string.Empty;
247280

248281
var jsonDoc =
249282
JsonDocument.Parse(PowertoolsLoggingSerializer.Serialize(eventArg, eventArg.GetType()));
250283

251284
var element = jsonDoc.RootElement;
252285

253-
for (var i = 0; i < correlationIdPaths.Length; i++)
286+
for (var i = 0; i < paths.Length; i++)
254287
{
255288
// For casing parsing to be removed from Logging v2 when we get rid of outputcase
256289
// without this CorrelationIdPaths.ApiGatewayRest would not work
257290
var pathWithOutputCase =
258-
_powertoolsConfigurations.ConvertToOutputCase(correlationIdPaths[i], _config.LoggerOutputCase);
291+
_powertoolsConfigurations.ConvertToOutputCase(paths[i], _config.LoggerOutputCase);
259292
if (!element.TryGetProperty(pathWithOutputCase, out var childElement))
260293
break;
261294

262295
element = childElement;
263-
if (i == correlationIdPaths.Length - 1)
264-
correlationId = element.ToString();
296+
if (i == paths.Length - 1)
297+
keyValue = element.ToString();
265298
}
266299

267-
if (!string.IsNullOrWhiteSpace(correlationId))
268-
Logger.AppendKey(LoggingConstants.KeyCorrelationId, correlationId);
300+
if (!string.IsNullOrWhiteSpace(keyValue))
301+
Logger.AppendKey(keyName, keyValue);
269302
}
270303
catch (Exception e)
271304
{

libraries/src/AWS.Lambda.Powertools.Logging/LoggingAttribute.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
*/
1515

1616
using System;
17+
using System.Collections;
18+
using System.Collections.Generic;
1719
using AspectInjector.Broker;
1820
using AWS.Lambda.Powertools.Logging.Internal;
1921
using Microsoft.Extensions.Logging;
@@ -93,6 +95,10 @@ namespace AWS.Lambda.Powertools.Logging;
9395
/// <description>string, pointer path to extract correlation id from input parameter</description>
9496
/// </item>
9597
/// <item>
98+
/// <term>ExtractedKeyPaths</term>
99+
/// <description>IEnumerable of pairs of pointer path to extract a value from input parameter, and the name of the key to store it in</description>
100+
/// </item>
101+
/// <item>
96102
/// <term>ClearState</term>
97103
/// <description>bool, clear all custom keys on each request, by default false</description>
98104
/// </item>
@@ -106,7 +112,8 @@ namespace AWS.Lambda.Powertools.Logging;
106112
/// ClearState = true,
107113
/// LogLevel = LogLevel.Debug,
108114
/// LoggerOutputCase = LoggerOutputCase.SnakeCase,
109-
/// CorrelationIdPath = "/headers/my_request_id_header")
115+
/// CorrelationIdPath = "/headers/my_request_id_header"),
116+
/// ExtractedKeyPaths = [("/headers/my_request_id", device_id"), ("/headers/user_id", "user_id")]
110117
/// ]
111118
/// public async Task&lt;APIGatewayProxyResponse&gt; FunctionHandler
112119
/// (APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
@@ -156,6 +163,18 @@ public class LoggingAttribute : Attribute
156163
/// </summary>
157164
/// <value>The correlation identifier path.</value>
158165
public string CorrelationIdPath { get; set; }
166+
167+
/// <summary>
168+
/// Pointer paths to extract other information from input parameter and the name of the key store it in.
169+
/// For example ("/headers/device_id", "DeviceId") will extract the value from that path and add the value to
170+
/// the key named "DeviceId".
171+
/// The first handler parameter is the input to the handler, which can be
172+
/// event data (published by an event source) or custom input that you provide
173+
/// such as a string or any custom data object.
174+
/// Use the same path naming convention as CorrelationIdPath.
175+
/// </summary>
176+
/// <value>IEnumerable of pairs of the Path to the data, and the name of the key to store it in.</value>
177+
public IEnumerable<(string Path, string KeyName)> ExtractedKeyPaths { get; set; }
159178

160179
/// <summary>
161180
/// Logger is commonly initialized in the global scope.

libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Attributes/LoggerAspectTests.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,17 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable()
263263
var name = "TestMethod";
264264
var args = new object[]
265265
{
266-
new TestObject { FullName = "Powertools", Age = 20, Headers = new Header { MyRequestIdHeader = "test" } }
266+
new TestObject
267+
{
268+
FullName = "Powertools",
269+
Age = 20,
270+
Headers = new Header
271+
{
272+
MyRequestIdHeader = "test",
273+
MySecondRequestIdHeader = "test2",
274+
MyThirdRequestIdHeader = "test3"
275+
}
276+
}
267277
};
268278
var hostType = typeof(string);
269279
var method = typeof(TestHandlers).GetMethod("TestMethod");
@@ -275,7 +285,9 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable()
275285
Service = "TestService",
276286
LoggerOutputCase = LoggerOutputCase.PascalCase,
277287
LogEvent = true,
278-
CorrelationIdPath = "/Headers/MyRequestIdHeader"
288+
CorrelationIdPath = "/Headers/MyRequestIdHeader",
289+
ExtractedKeyPaths = [("/Headers/MySecondRequestIdHeader", "second"),
290+
("/Headers/MyThirdRequestIdHeader", "third")]
279291
}
280292
};
281293

@@ -298,8 +310,10 @@ public void OnEntry_Should_LogDebug_WhenSet_EnvironmentVariable()
298310

299311
_mockSystemWrapper.Received(1).LogLine(Arg.Is<string>(s =>
300312
s.Contains("\"CorrelationId\":\"test\"") &&
313+
s.Contains("\"Second\":\"test2\"") &&
314+
s.Contains("\"Third\":\"test3\"") &&
301315
s.Contains(
302-
"\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":{\"MyRequestIdHeader\":\"test\"}")
316+
"\"Message\":{\"FullName\":\"Powertools\",\"Age\":20,\"Headers\":{\"MyRequestIdHeader\":\"test\",\"MySecondRequestIdHeader\":\"test2\",\"MyThirdRequestIdHeader\":\"test3\"}")
303317
));
304318
}
305319

libraries/tests/AWS.Lambda.Powertools.Logging.Tests/Serializers/TestJsonContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ internal class TestObject
2626
internal class Header
2727
{
2828
public string MyRequestIdHeader { get; set; }
29+
public string MySecondRequestIdHeader { get; set; }
30+
public string MyThirdRequestIdHeader { get; set; }
2931
}

0 commit comments

Comments
 (0)