Skip to content
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

Update documentation #345

Merged
merged 3 commits into from
Dec 26, 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
32 changes: 16 additions & 16 deletions docs/usage/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ Echopraxia is simple and easy to use, and looks very similar to SLF4J.
Add the import:

```java

import echopraxia.api.*;
import echopraxia.logger.*;
```

Define a logger (usually in a controller or singleton -- `getClass()` is particularly useful for abstract controllers):

```java
final Logger<FieldBuilder> basicLogger = LoggerFactory.getLogger(getClass());
import echopraxia.simple.*;

final Logger basicLogger = LoggerFactory.getLogger(getClass());
```

Logging simple messages and exceptions are done as in SLF4J:
Expand All @@ -29,13 +32,14 @@ try {
However, when you log arguments, you pass a function which provides you with a customizable field builder and returns a `FieldBuilderResult` -- a `Field` is a `FieldBuilderResult`, so you can do:

```java
basicLogger.info("Message name {}", fb -> fb.string("name", "value"));
var fb = FieldBuilder.instance();
basicLogger.info("Message name {}", fb.string("name", "value"));
```

If you are returning multiple fields, then using `fb.list` will return a `FieldBuilderResult`:

```java
basicLogger.info("Message name {} age {}", fb -> fb.list(
basicLogger.info("Message name {} age {}", fb.list(
fb.string("name", "value"),
fb.number("age", 13)
));
Expand All @@ -44,10 +48,8 @@ basicLogger.info("Message name {} age {}", fb -> fb.list(
And `fb.list` can take many inputs as needed, for example a stream:

```java
basicLogger.info("Message name {}", fb -> {
Stream<Field> fieldStream = ...;
return fb.list(arrayOfFields);
});
var arrayOfFields = { fb.string("name", "value") };
basicLogger.info("Message name {}", fb.list(arrayOfFields));
```

The field builder is customizable, so you can (and should!) define your own methods to construct fields out of complex objects:
Expand All @@ -56,6 +58,7 @@ The field builder is customizable, so you can (and should!) define your own meth
class OrderFieldBuilder extends FieldBuilder {
// Use apply to render order as a Field
public Field apply(Order order) {
// assume apply methods for line items etc
return keyValue("order", Value.object(
apply(order.lineItems),
apply(order.paymentInfo),
Expand All @@ -65,15 +68,16 @@ class OrderFieldBuilder extends FieldBuilder {
}
}

logger.info("Rendering order {}", fb -> fb.apply(order));
var fb = new OrderFieldBuilder();
logger.info("Rendering order {}", fb.apply(order));
```

Please read the [field builder](fieldbuilder.md) section for more information on making your own field builder methods.

You can log multiple arguments and include the exception if you want the stack trace:

```java
basicLogger.info("Message name {}", fb -> fb.list(
basicLogger.info("Message name {}", fb.list(
fb.string("name", "value"),
fb.exception(e)
));
Expand All @@ -82,15 +86,11 @@ basicLogger.info("Message name {}", fb -> fb.list(
You can also create the fields yourself and pass them in directly:

```java


var fb = FieldBuilder.instance;
var fb = FieldBuilder.instance();
var nameField = fb.string("name", "value");
var ageField = fb.number("age", 13);
var exceptionField = fb.exception(e);
logger.

info(nameField, ageField, exceptionField);
logger.info(nameField, ageField, exceptionField);
```

Note that unlike SLF4J, you don't have to worry about including the exception as an argument "swallowing" the stacktrace. If an exception is present, it's always applied to the underlying logger.
Expand Down
32 changes: 21 additions & 11 deletions docs/usage/conditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Conditions are a great way to manage diagnostic logging in your application with
For example, if you want to have logging that only activates during business hours, you can use the following:

```java
import com.tersesystems.echopraxia.Condition;
import echopraxia.logging.api.Condition;

public class MyBusinessConditions {
private static final Clock officeClock = Clock.system(ZoneId.of("America/Los_Angeles")) ;
Expand Down Expand Up @@ -63,7 +63,15 @@ This is only a part of the available functionality in conditions. You can tie c

## JSON Path

In situations where you're looking through fields for a condition, you can use [JSONPath](https://github.com/json-path/JsonPath#jayway-jsonpath) to find values from the logging context in a condition.
If you are using the Logstash implementation, you can use the `echopraxia.jsonpath.JsonPathCondition.pathCondition()` method to provide you with an extended context that has logging methods:

This will give you a context that extends `FindPathMethods` that will let you use [JSONPath](https://github.com/json-path/JsonPath#jayway-jsonpath) to find values from the logging context in a condition.

```java
Condition fooCondition = pathCondition((level, ctx) ->
ctx.findString("$.foo").filter(s -> s.equals("bar")).isPresent()
);
```

Tip: if you are using IntelliJ IDEA, you can add the [@Language("JSONPath")](https://www.jetbrains.com/help/idea/using-language-injections.html#language_annotation) annotation to [inject JSONPATH](https://www.jetbrains.com/idea/guide/tips/evaluate-json-path-expressions/).

Expand Down Expand Up @@ -128,7 +136,7 @@ The inline and filter predicates are not available for exceptions. Instead, you
```java
class FindException {
void logException() {
Condition throwableCondition =
Condition throwableCondition = json
(level, ctx) ->
ctx.findThrowable()
.filter(e -> "test message".equals(e.getMessage()))
Expand All @@ -152,7 +160,7 @@ var loggerWithCondition = logger.withCondition(condition);
You can also build up conditions:

```java
Logger<FieldBuilder> loggerWithAandB = logger.withCondition(conditionA).withCondition(conditionB);
Logger loggerWithAandB = logger.withCondition(conditionA).withCondition(conditionB);
```

Conditions are only evaluated once a level/marker check is passed, so something like
Expand All @@ -166,8 +174,10 @@ will short circuit on the level check before any condition is reached.
Conditions look for fields, but those fields can come from *either* context or argument. For example, the following condition will log because the condition finds an argument field:

```java
Condition cond = (level, ctx) -> ctx.findString("somename").isPresent();
logger.withCondition(cond).info("some message", fb -> fb.string("somename", "somevalue")); // matches argument
import static echopraxia.jsonpath.JsonPathCondition.*;

Condition cond = pathCondition((level, ctx) -> ctx.findString("somename").isPresent());
logger.withCondition(cond).info("some message", fb.string("somename", "somevalue")); // matches argument
```

## Statement
Expand All @@ -194,18 +204,18 @@ A condition may also evaluate context fields that are set in a logger:

```java
// Conditions may evaluate context
Condition cond = (level, ctx) -> ctx.findString("somename").isPresent();
Condition cond = pathCondition((level, ctx) -> ctx.findString("somename").isPresent());
boolean loggerEnabled = logger
.withFields(fb -> fb.string("somename", "somevalue"))
.withFields( fb.string("somename", "somevalue"))
.withCondition(condition)
.isInfoEnabled();
```

Using a predicate with a condition does not trigger any logging, so it can be a nice way to "dry run" a condition. Note that the context evaluation takes place every time a condition is run, so doing something like this is not good:

```java
var loggerWithContextAndCondition = logger
.withFields(fb -> fb.string("somename", "somevalue"))
var loggerWithContextAndCondition = logger
.withFields( fb.string("somename", "somevalue"))
.withCondition(condition);

// check evaluates context
Expand All @@ -221,7 +231,7 @@ It is generally preferable to pass in a condition explicitly on the statement, a

```java
var loggerWithContext = logger
.withFields(fb -> fb.string("somename", "somevalue"));
.withFields( fb.string("somename", "somevalue"));
loggerWithContext.info(condition, "message");
```

Expand Down
11 changes: 5 additions & 6 deletions docs/usage/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
You can also add fields directly to the logger using `logger.withFields` for contextual logging:

```java
var loggerWithFoo = basicLogger.withFields(fb -> fb.string("foo", "bar"));
var loggerWithFoo = basicLogger.withFields(fb.string("foo", "bar"));

// will log "foo": "bar" field in a JSON appender.
loggerWithFoo.info("JSON field will log automatically")
Expand All @@ -20,10 +20,9 @@ public class PlayerData {
private Date lastAccessedDate = new Date();

// logger is not static because lastAccessedDate is an instance variable
private final Logger<BuilderWithDate> logger =
private final Logger logger =
LoggerFactory.getLogger()
.withFieldBuilder(BuilderWithDate.class)
.withFields(fb -> fb.date("last_accessed_date", lastAccessedDate));
.withFields(fb.date("last_accessed_date", lastAccessedDate));

}
```
Expand Down Expand Up @@ -63,7 +62,7 @@ For example, `SimpleDateFormat` is infamously not thread-safe, and so the follow
private final static DateFormat df = new SimpleDateFormat("yyyyMMdd");

// UNSAFE EXAMPLE
private static final Logger<FieldBuilder> logger =
private static final Logger logger =
LoggerFactory.getLogger()
.withFields(fb -> fb.string("unsafe_date", df.format(new Date())));
.withFields(fb.string("unsafe_date", df.format(new Date())));
```
48 changes: 20 additions & 28 deletions docs/usage/fieldbuilder.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Conceptually, a field builder is a handle for creating structured data. The `Lo
Start by importing the API package. Everything relevant to field building will be in there.

```java

import echopraxia.api.*;
```

## Defining Field Builders
Expand Down Expand Up @@ -56,36 +56,27 @@ public interface BuilderWithDate implements FieldBuilder {
}
```

And then create a `Logger<BuilderWithDate>`:
And then create a `Logger`:

```java
Logger<BuilderWithDate> dateLogger = LoggerFactory.getLogger(YourService.class, BuilderWithDate.instance);
var fb = new BuilderWithDate();
Logger dateLogger = LoggerFactory.getLogger(YourService.class);
```

And now you can render a date automatically:

```java
dateLogger.info("Date {}", fb -> fb.date("creation_date", new Date()));

dateLogger.info("Date {}", fb.date("creation_date", new Date()));
```

### Exception Handling

Avoid throwing exceptions in a field builder function.

```java
logger.info("Message name {}", fb -> {
String name = methodThatThrowsException(); // BAD
return fb.string(name, "some-value");
});
```

Instead, only call field builder methods inside the closure and keep any construction logic outside:

```java
String name = methodThatThrowsException(); // GOOD
logger.info("Message name {}", fb -> {
return fb.string(name, "some-value");
});
logger.info("Message name {}", fb.string(name, "some-value"));
```

If an exception is thrown it will be caught by Echopraxia's default `ExceptionHandler` which writes the exception to `System.err` by default. You can provide your own behavior for the `ExceptionHandler` using a service loader pattern.
Expand All @@ -112,9 +103,8 @@ public interface NullableFieldBuilder extends FieldBuilder {
Field names are never allowed to be null. If a field name is null, it will be replaced at runtime with `unknown-echopraxia-N` where N is an incrementing number.

```java
logger.info("Message name {}", fb ->
fb.string(null, "some-value") // null field names not allowed
);
// null field names not allowed
logger.info("Message name {}",fb.string(null, "some-value"));
```

### Complex Objects
Expand Down Expand Up @@ -153,8 +143,8 @@ And then you can render a person:

```java
Person user = ...
Logger<PersonFieldBuilder> personLogger = basicLogger.withFieldBuilder(PersonFieldBuilder.instance);
personLogger.info("Person {}", fb -> fb.person("user", user));
var fb = PersonFieldBuilder.instance;
basicLogger.info("Person {}", fb.person("user", user));
```

## Field Presentation
Expand Down Expand Up @@ -355,7 +345,7 @@ Even if you turn `ignore_malformed` on or have different mappings, a change in a
Likewise, field names are not automatically scoped by context. You may have collision cases where two different fields have the same name in the same statement:

```java
logger.withFields(fb -> fb.keyValue("user_id", userId)).info("{}", fb -> fb.keyValue("user_id", otherUserId));
logger.withFields(fb.keyValue("user_id", userId)).info("{}", fb.keyValue("user_id", otherUserId));
```

This will produce a statement that has two `user_id` fields with two different values -- which is technically valid JSON, but may not be what centralized logging expects. You can qualify your arguments by adding a [nested](https://github.com/logfellow/logstash-logback-encoder#nested-json-provider), or add logic that will validate/reject/clean invalid fields, but it may be simpler to explicitly pass in distinct names or namespace with `fb.object` or `Value.object`.
Expand Down Expand Up @@ -416,7 +406,7 @@ So, define a field builder per package:
package com.mystore.user;

// field builder for the user package:
interface UserFieldBuilder extends FieldBuilder {
public interface UserFieldBuilder extends FieldBuilder {
UserFieldBuilder instance = new UserFieldBuilder() {};

default Field user(User user) {
Expand All @@ -425,13 +415,13 @@ interface UserFieldBuilder extends FieldBuilder {
}

public abstract class LoggingBase {
protected static final Logger<UserFieldBuilder> logger =
LoggerFactory.getLogger(this.getClass(), UserFieldBuilder.instance);
protected static final Logger logger = LoggerFactory.getLogger(this.getClass());
protected UserFieldBuilder fb = fieldBuilder();
}

public class SomeUserService extends LoggingBase {
public void someMethod(User user) {
logger.trace("someMethod: {}", fb -> fb.user(user));
logger.trace("someMethod: {}", fb.user(user));
}
}
```
Expand All @@ -458,10 +448,12 @@ interface OrderFieldBuilder extends UserFieldBuilder {
}

public class SomeOrderService {
private static final Logger<OrderFieldBuilder> logger = LoggerFactory.getLogger(SomeOrderService.class, OrderFieldBuilder.instance);
private static final Logger logger = LoggerFactory.getLogger(SomeOrderService.class);

private var fb = OrderFieldBuilder.instance;

public void someMethod(Order order) {
logger.trace("someMethod: {}", fb -> fb.order(order));
logger.trace("someMethod: {}", fb.order(order));
}
}
```
Expand Down
26 changes: 13 additions & 13 deletions docs/usage/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ package example;
public class ExampleFilter implements CoreLoggerFilter {
@Override
public CoreLogger apply(CoreLogger coreLogger) {
return coreLogger
.withFields(fb -> fb.bool("uses_filter", true), FieldBuilder.instance());
var fb = FieldBuilder.instance();
return coreLogger.withFields(fb.bool("uses_filter", true));
}
}
```
Expand Down Expand Up @@ -48,17 +48,17 @@ public class SystemInfoFilter implements CoreLoggerFilter {

// Now you can add conditions based on these fields, and conditionally
// enable logging based on your load and memory!
return coreLogger.withFields(fb -> {
Field loadField = fb.object("load_average", //
fb.number("1min", loadAverage[0]), //
fb.number("5min", loadAverage[1]), //
fb.number("15min", loadAverage[2]));
Field memField = fb.object("mem", //
fb.number("available", mem.getAvailable()), //
fb.number("total", mem.getTotal()));
Field sysinfoField = fb.object("sysinfo", loadField, memField);
return sysinfoField;
}, FieldBuilder.instance());

Field loadField = fb.object("load_average", //
fb.number("1min", loadAverage[0]), //
fb.number("5min", loadAverage[1]), //
fb.number("15min", loadAverage[2]));
Field memField = fb.object("mem", //
fb.number("available", mem.getAvailable()), //
fb.number("total", mem.getTotal()));
Field sysinfoField = fb.object("sysinfo", loadField, memField);

return coreLogger.withFields(sysinfoField);
}
}
```
Expand Down
Loading
Loading