Skip to content

Commit

Permalink
Merge pull request #109 from nblumhardt/capture-function
Browse files Browse the repository at this point in the history
Add the `Inspect()` function
  • Loading branch information
nblumhardt authored Jun 4, 2024
2 parents b42533b + 47d8bee commit ed917a8
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 26 deletions.
51 changes: 28 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ The following properties are available in expressions:

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

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

### Literals

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

| Function | Description |
| :--- | :--- |
| `Coalesce(p0, p1, [..pN])` | Returns the first defined, non-null argument. |
| `Concat(s0, s1, [..sN])` | Concatenate two or more strings. |
| `Contains(s, t)` | Tests whether the string `s` contains the substring `t`. |
| `ElementAt(x, i)` | Retrieves a property of `x` by name `i`, or array element of `x` by numeric index `i`. |
| `EndsWith(s, t)` | Tests whether the string `s` ends with substring `t`. |
| `IndexOf(s, t)` | Returns the first index of substring `t` in string `s`, or -1 if the substring does not appear. |
| `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. |
| `IsMatch(s, p)` | Tests whether the regular expression `p` matches within the string `s`. |
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
| `Length(x)` | Returns the length of a string or array. |
| `Now()` | Returns `DateTimeOffset.Now`. |
| `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. |
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
| `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. |
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
| `ToString(x, [format])` | Convert `x` to a string, applying the format string `format` if `x` is `IFormattable`. |
| `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'`. |
| `Undefined()` | Explicitly mark an undefined value. |
| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
| Function | Description |
|:--------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Coalesce(p0, p1, [..pN])` | Returns the first defined, non-null argument. |
| `Concat(s0, s1, [..sN])` | Concatenate two or more strings. |
| `Contains(s, t)` | Tests whether the string `s` contains the substring `t`. |
| `ElementAt(x, i)` | Retrieves a property of `x` by name `i`, or array element of `x` by numeric index `i`. |
| `EndsWith(s, t)` | Tests whether the string `s` ends with substring `t`. |
| `IndexOf(s, t)` | Returns the first index of substring `t` in string `s`, or -1 if the substring does not appear. |
| `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. |
| `Inspect(o, [deep])` | Read properties from an object captured as the scalar value `o`. |
| `IsMatch(s, p)` | Tests whether the regular expression `p` matches within the string `s`. |
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
| `Length(x)` | Returns the length of a string or array. |
| `Now()` | Returns `DateTimeOffset.Now`. |
| `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. |
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
| `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. |
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
| `ToString(x, [format])` | Convert `x` to a string, applying the format string `format` if `x` is `IFormattable`. |
| `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'`. |
| `Undefined()` | Explicitly mark an undefined value. |
| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |

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

Expand Down
1 change: 1 addition & 0 deletions src/Serilog.Expressions/Expressions/Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ static class Operators
public const string OpEndsWith = "EndsWith";
public const string OpIndexOf = "IndexOf";
public const string OpIndexOfMatch = "IndexOfMatch";
public const string OpInspect = "Inspect";
public const string OpIsMatch = "IsMatch";
public const string OpIsDefined = "IsDefined";
public const string OpLastIndexOf = "LastIndexOf";
Expand Down
41 changes: 40 additions & 1 deletion src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Reflection;
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Expressions.Compilation.Linq;
using Serilog.Templates.Rendering;
Expand Down Expand Up @@ -538,4 +540,41 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
// DateTimeOffset.Now is the generator for LogEvent.Timestamp.
return new ScalarValue(DateTimeOffset.Now);
}
}

public static LogEventPropertyValue? Inspect(LogEventPropertyValue? value, LogEventPropertyValue? deep = null)
{
if (value is not ScalarValue { Value: {} toCapture })
return value;

var result = new List<LogEventProperty>();
var logger = new LoggerConfiguration().CreateLogger();
var properties = toCapture.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);

foreach (var property in properties)
{
object? p;
try
{
p = property.GetValue(toCapture);
}
catch (Exception ex)
{
SelfLog.WriteLine("Serilog.Expressions Inspect() target property threw exception: {0}", ex);
continue;
}

if (deep is ScalarValue { Value: true })
{
if (logger.BindProperty(property.Name, p, destructureObjects: true, out var bound))
result.Add(bound);
}
else
{
result.Add(new LogEventProperty(property.Name, new ScalarValue(p)));
}
}

return new StructureValue(result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ typeof(true) ⇶ 'System.Boolean'
typeof(null) ⇶ 'null'
typeof([]) ⇶ 'array'
typeof({}) ⇶ 'object'
typeof(@x) ⇶ 'System.DivideByZeroException'

// UtcDateTime
tostring(utcdatetime(now()), 'o') like '20%' ⇶ true
Expand Down Expand Up @@ -313,3 +314,6 @@ tostring(@x) like 'System.DivideByZeroException%' ⇶ true
@l ⇶ 'Warning'
@sp ⇶ 'bb1111820570b80e'
@tr ⇶ '1befc31e94b01d1a473f63a7905f6c9b'

// Inspect
inspect(@x).Message ⇶ 'Attempted to divide by zero.'
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Reflection;
using Serilog.Events;
using Serilog.Expressions.Runtime;
using Serilog.Expressions.Tests.Support;
using Xunit;

namespace Serilog.Expressions.Tests.Expressions.Runtime;

public class RuntimeOperatorsTests
{
[Fact]
public void InspectReadsPublicPropertiesFromScalarValue()
{
var message = Some.String();
var ex = new DivideByZeroException(message);
var scalar = new ScalarValue(ex);
var inspected = RuntimeOperators.Inspect(scalar);
var structure = Assert.IsType<StructureValue>(inspected);
var asProperties = structure.Properties.ToDictionary(p => p.Name, p => p.Value);
Assert.Contains("Message", asProperties);
Assert.Contains("StackTrace", asProperties);
var messageResult = Assert.IsType<ScalarValue>(asProperties["Message"]);
Assert.Equal(message, messageResult.Value);
}

[Fact]
public void DeepInspectionReadsSubproperties()
{
var innerMessage = Some.String();
var inner = new DivideByZeroException(innerMessage);
var ex = new TargetInvocationException(inner);
var scalar = new ScalarValue(ex);
var inspected = RuntimeOperators.Inspect(scalar, deep: new ScalarValue(true));
var structure = Assert.IsType<StructureValue>(inspected);
var innerStructure = Assert.IsType<StructureValue>(structure.Properties.Single(p => p.Name == "InnerException").Value);
var innerMessageValue = Assert.IsType<ScalarValue>(innerStructure.Properties.Single(p => p.Name == "Message").Value);
Assert.Equal(innerMessage, innerMessageValue.Value);
}
}
16 changes: 14 additions & 2 deletions test/Serilog.Expressions.Tests/Support/Some.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Serilog.Expressions.Tests.Support;

static class Some
{
static int _next;

public static LogEvent InformationEvent(string messageTemplate = "Hello, world!", params object?[] propertyValues)
{
return LogEvent(LogEventLevel.Information, messageTemplate, propertyValues);
Expand All @@ -29,11 +31,21 @@ public static LogEvent LogEvent(LogEventLevel level, string messageTemplate = "H

public static object AnonymousObject()
{
return new {A = 42};
return new {A = Int()};
}

public static LogEventPropertyValue LogEventPropertyValue()
{
return new ScalarValue(AnonymousObject());
}
}

static int Int()
{
return Interlocked.Increment(ref _next);
}

public static string String()
{
return $"+S_{Int()}";
}
}

0 comments on commit ed917a8

Please sign in to comment.