Skip to content

Add the Inspect() function #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 28 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,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 @@ -181,29 +185,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()}";
}
}