Skip to content

Commit ed917a8

Browse files
authored
Merge pull request #109 from nblumhardt/capture-function
Add the `Inspect()` function
2 parents b42533b + 47d8bee commit ed917a8

File tree

6 files changed

+126
-26
lines changed

6 files changed

+126
-26
lines changed

README.md

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ The following properties are available in expressions:
137137

138138
The built-in properties mirror those available in the CLEF format.
139139

140+
The exception property `@x` is treated as a scalar and will appear as a string when formatted into text. The properties of
141+
the underlying `Exception` object can be accessed using `Inspect()`, for example `Inspect(@x).Message`, and the type of the
142+
exception retrieved using `TypeOf(@x)`.
143+
140144
### Literals
141145

142146
| Data type | Description | Examples |
@@ -183,29 +187,30 @@ calling a function will be undefined if:
183187
* any argument is undefined, or
184188
* any argument is of an incompatible type.
185189

186-
| Function | Description |
187-
| :--- | :--- |
188-
| `Coalesce(p0, p1, [..pN])` | Returns the first defined, non-null argument. |
189-
| `Concat(s0, s1, [..sN])` | Concatenate two or more strings. |
190-
| `Contains(s, t)` | Tests whether the string `s` contains the substring `t`. |
191-
| `ElementAt(x, i)` | Retrieves a property of `x` by name `i`, or array element of `x` by numeric index `i`. |
192-
| `EndsWith(s, t)` | Tests whether the string `s` ends with substring `t`. |
193-
| `IndexOf(s, t)` | Returns the first index of substring `t` in string `s`, or -1 if the substring does not appear. |
194-
| `IndexOfMatch(s, p)` | Returns the index of the first match of regular expression `p` in string `s`, or -1 if the regular expression does not match. |
195-
| `IsMatch(s, p)` | Tests whether the regular expression `p` matches within the string `s`. |
196-
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
197-
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
198-
| `Length(x)` | Returns the length of a string or array. |
199-
| `Now()` | Returns `DateTimeOffset.Now`. |
200-
| `Rest([deep])` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template. If `deep` is `true`, also excludes properties referenced in the event's message template. |
201-
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
202-
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
203-
| `Substring(s, start, [length])` | Return the substring of string `s` from `start` to the end of the string, or of `length` characters, if this argument is supplied. |
204-
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
205-
| `ToString(x, [format])` | Convert `x` to a string, applying the format string `format` if `x` is `IFormattable`. |
206-
| `TypeOf(x)` | Returns a string describing the type of expression `x`: a .NET type name if `x` is scalar and non-null, or, `'array'`, `'object'`, `'dictionary'`, `'null'`, or `'undefined'`. |
207-
| `Undefined()` | Explicitly mark an undefined value. |
208-
| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
190+
| Function | Description |
191+
|:--------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
192+
| `Coalesce(p0, p1, [..pN])` | Returns the first defined, non-null argument. |
193+
| `Concat(s0, s1, [..sN])` | Concatenate two or more strings. |
194+
| `Contains(s, t)` | Tests whether the string `s` contains the substring `t`. |
195+
| `ElementAt(x, i)` | Retrieves a property of `x` by name `i`, or array element of `x` by numeric index `i`. |
196+
| `EndsWith(s, t)` | Tests whether the string `s` ends with substring `t`. |
197+
| `IndexOf(s, t)` | Returns the first index of substring `t` in string `s`, or -1 if the substring does not appear. |
198+
| `IndexOfMatch(s, p)` | Returns the index of the first match of regular expression `p` in string `s`, or -1 if the regular expression does not match. |
199+
| `Inspect(o, [deep])` | Read properties from an object captured as the scalar value `o`. |
200+
| `IsMatch(s, p)` | Tests whether the regular expression `p` matches within the string `s`. |
201+
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
202+
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
203+
| `Length(x)` | Returns the length of a string or array. |
204+
| `Now()` | Returns `DateTimeOffset.Now`. |
205+
| `Rest([deep])` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template. If `deep` is `true`, also excludes properties referenced in the event's message template. |
206+
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
207+
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
208+
| `Substring(s, start, [length])` | Return the substring of string `s` from `start` to the end of the string, or of `length` characters, if this argument is supplied. |
209+
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
210+
| `ToString(x, [format])` | Convert `x` to a string, applying the format string `format` if `x` is `IFormattable`. |
211+
| `TypeOf(x)` | Returns a string describing the type of expression `x`: a .NET type name if `x` is scalar and non-null, or, `'array'`, `'object'`, `'dictionary'`, `'null'`, or `'undefined'`. |
212+
| `Undefined()` | Explicitly mark an undefined value. |
213+
| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
209214

210215
Functions that compare text accept an optional postfix `ci` modifier to select case-insensitive comparisons:
211216

src/Serilog.Expressions/Expressions/Operators.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ static class Operators
3333
public const string OpEndsWith = "EndsWith";
3434
public const string OpIndexOf = "IndexOf";
3535
public const string OpIndexOfMatch = "IndexOfMatch";
36+
public const string OpInspect = "Inspect";
3637
public const string OpIsMatch = "IsMatch";
3738
public const string OpIsDefined = "IsDefined";
3839
public const string OpLastIndexOf = "LastIndexOf";

src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Reflection;
16+
using Serilog.Debugging;
1517
using Serilog.Events;
1618
using Serilog.Expressions.Compilation.Linq;
1719
using Serilog.Templates.Rendering;
@@ -538,4 +540,41 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
538540
// DateTimeOffset.Now is the generator for LogEvent.Timestamp.
539541
return new ScalarValue(DateTimeOffset.Now);
540542
}
541-
}
543+
544+
public static LogEventPropertyValue? Inspect(LogEventPropertyValue? value, LogEventPropertyValue? deep = null)
545+
{
546+
if (value is not ScalarValue { Value: {} toCapture })
547+
return value;
548+
549+
var result = new List<LogEventProperty>();
550+
var logger = new LoggerConfiguration().CreateLogger();
551+
var properties = toCapture.GetType()
552+
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);
553+
554+
foreach (var property in properties)
555+
{
556+
object? p;
557+
try
558+
{
559+
p = property.GetValue(toCapture);
560+
}
561+
catch (Exception ex)
562+
{
563+
SelfLog.WriteLine("Serilog.Expressions Inspect() target property threw exception: {0}", ex);
564+
continue;
565+
}
566+
567+
if (deep is ScalarValue { Value: true })
568+
{
569+
if (logger.BindProperty(property.Name, p, destructureObjects: true, out var bound))
570+
result.Add(bound);
571+
}
572+
else
573+
{
574+
result.Add(new LogEventProperty(property.Name, new ScalarValue(p)));
575+
}
576+
}
577+
578+
return new StructureValue(result);
579+
}
580+
}

test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ typeof(true) ⇶ 'System.Boolean'
264264
typeof(null) ⇶ 'null'
265265
typeof([]) ⇶ 'array'
266266
typeof({}) ⇶ 'object'
267+
typeof(@x) ⇶ 'System.DivideByZeroException'
267268

268269
// UtcDateTime
269270
tostring(utcdatetime(now()), 'o') like '20%' ⇶ true
@@ -313,3 +314,6 @@ tostring(@x) like 'System.DivideByZeroException%' ⇶ true
313314
@l ⇶ 'Warning'
314315
@sp ⇶ 'bb1111820570b80e'
315316
@tr ⇶ '1befc31e94b01d1a473f63a7905f6c9b'
317+
318+
// Inspect
319+
inspect(@x).Message ⇶ 'Attempted to divide by zero.'
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Reflection;
2+
using Serilog.Events;
3+
using Serilog.Expressions.Runtime;
4+
using Serilog.Expressions.Tests.Support;
5+
using Xunit;
6+
7+
namespace Serilog.Expressions.Tests.Expressions.Runtime;
8+
9+
public class RuntimeOperatorsTests
10+
{
11+
[Fact]
12+
public void InspectReadsPublicPropertiesFromScalarValue()
13+
{
14+
var message = Some.String();
15+
var ex = new DivideByZeroException(message);
16+
var scalar = new ScalarValue(ex);
17+
var inspected = RuntimeOperators.Inspect(scalar);
18+
var structure = Assert.IsType<StructureValue>(inspected);
19+
var asProperties = structure.Properties.ToDictionary(p => p.Name, p => p.Value);
20+
Assert.Contains("Message", asProperties);
21+
Assert.Contains("StackTrace", asProperties);
22+
var messageResult = Assert.IsType<ScalarValue>(asProperties["Message"]);
23+
Assert.Equal(message, messageResult.Value);
24+
}
25+
26+
[Fact]
27+
public void DeepInspectionReadsSubproperties()
28+
{
29+
var innerMessage = Some.String();
30+
var inner = new DivideByZeroException(innerMessage);
31+
var ex = new TargetInvocationException(inner);
32+
var scalar = new ScalarValue(ex);
33+
var inspected = RuntimeOperators.Inspect(scalar, deep: new ScalarValue(true));
34+
var structure = Assert.IsType<StructureValue>(inspected);
35+
var innerStructure = Assert.IsType<StructureValue>(structure.Properties.Single(p => p.Name == "InnerException").Value);
36+
var innerMessageValue = Assert.IsType<ScalarValue>(innerStructure.Properties.Single(p => p.Name == "Message").Value);
37+
Assert.Equal(innerMessage, innerMessageValue.Value);
38+
}
39+
}

test/Serilog.Expressions.Tests/Support/Some.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ namespace Serilog.Expressions.Tests.Support;
55

66
static class Some
77
{
8+
static int _next;
9+
810
public static LogEvent InformationEvent(string messageTemplate = "Hello, world!", params object?[] propertyValues)
911
{
1012
return LogEvent(LogEventLevel.Information, messageTemplate, propertyValues);
@@ -29,11 +31,21 @@ public static LogEvent LogEvent(LogEventLevel level, string messageTemplate = "H
2931

3032
public static object AnonymousObject()
3133
{
32-
return new {A = 42};
34+
return new {A = Int()};
3335
}
3436

3537
public static LogEventPropertyValue LogEventPropertyValue()
3638
{
3739
return new ScalarValue(AnonymousObject());
3840
}
39-
}
41+
42+
static int Int()
43+
{
44+
return Interlocked.Increment(ref _next);
45+
}
46+
47+
public static string String()
48+
{
49+
return $"+S_{Int()}";
50+
}
51+
}

0 commit comments

Comments
 (0)