Skip to content

Conditions

Logging conditions can be handled gracefully using Condition functions. A Condition will take a Level and a LoggingContext which will return the fields of the logger.

final Condition errorCondition = new Condition() {
  @Override
  public boolean test(Level level, LoggingContext context) {
    return level.equals(Level.ERROR);
  }
};

Conditions can be used either on the logger, on the statement, or against the predicate check.

There are two elemental conditions, Condition.always() and Condition.never(). Echopraxia has optimizations for conditions; it will treat Condition.always() as a no-op, and return a NeverLogger that has no operations for logging. The JVM can recognize that logging has no effect at all, and will eliminate the method call as dead code.

Conditions are a great way to manage diagnostic logging in your application with more flexibility than global log levels can provide. Consider enabling setting your application logging to DEBUG i.e. <logger name="your.application.package" level="DEBUG"/> and using conditions to turn on and off debugging as needed. Conditions come with and, or, and xor functionality, and can provide more precise and expressive logging criteria than can be managed with filters and markers. This is particularly useful when combined with filters.

For example, if you want to have logging that only activates during business hours, you can use the following:

import com.tersesystems.echopraxia.Condition;

public class MyBusinessConditions {
  private static final Clock officeClock = Clock.system(ZoneId.of("America/Los_Angeles")) ;

  public Condition businessHoursOnly() {
    return Condition.operational().and(weekdays().and(from9to5()));
  }

  public Condition weekdays() {
    return (level, context) -> {
      LocalDate now = LocalDate.now(officeClock);
      final DayOfWeek dayOfWeek = now.getDayOfWeek();
      return ! (dayOfWeek.equals(DayOfWeek.SATURDAY) || dayOfWeek.equals(DayOfWeek.SUNDAY));
    };
  }

  public Condition from9to5() {
    return (level, context) -> LocalTime.now(officeClock).query(temporal -> {
      // hour is zero based, so adjust for readability
      final int hour = temporal.get(ChronoField.HOUR_OF_DAY) + 1;
      return (hour >= 9) && (hour <= 17); // 8 am to 5 pm
    });
  }
}

Matching values is best done using Value.equals in conjunction with one of the match methods:

// Better type safety using Value.equals
Condition hasDerp = Condition.stringMatch("herp", v -> Value.equals(v, Value.string("herp")))

// this works too
Condition logins = Condition.numberMatch("logins", v -> v.equals(number(1)));

The context parameter that is passed in is a LoggingContext that contains the argument fields, the fields added directly to the logger, and a reference to the CoreLogger, which can return useful context like the logger name.

This is only a part of the available functionality in conditions. You can tie conditions directly to a backend, such as a database or key/value store, or trigger them to work in response to an exception or unusual metrics. See the redis example, jmx example, metrics example, and timed diagnostic example.

JSON Path

In situations where you're looking through fields for a condition, you can use JSONPath to find values from the logging context in a condition.

Tip: if you are using IntelliJ IDEA, you can add the @Language("JSONPath") annotation to inject JSONPATH.

The context.find* methods take a class as a type, and a JSON path, which can be used to search through context fields (or arguments, if the condition is used in a logging statement).

The basic types are String, the Number subclasses such as Integer, and Boolean. If no matching path is found, an empty Optional is returned.

Optional<String> optName = context.findString("$.person.name");

This also applies to Throwable which are usually passed in as arguments:

Optional<Throwable> optThrowable = context.findThrowable();

You can treat a Throwable as a JSON object, i.e. the following will all work with the default $.exception path:

Optional<String> className = ctx.findString("$.exception.className");
Optional<String> message = ctx.findString("$.exception.message");
Optional<Throwable> cause = ctx.findThrowable("$.exception.cause");

And you can also query stack trace elements:

Optional<Map<String, ?>> stacktraceElement = ctx.findObject("$.exception.stackTrace[0]")
Optional<String> methodName = ctx.findString("$.exception.stackTrace[0].methodName");
Optional<List<?>> listOfElements = ctx.findObject("$.exception.stackTrace[5..10]")

Finding an explicitly null value returns a boolean:

// fb.nullValue("keyWithNullValue") sets an explicitly null value
boolean isNull = context.findNull("$.keyWithNullValue");

Finding an object will return a Map:

Optional<Map<String, ?>> mother = context.findObject("$.person.mother");

For a List, in the case of an array value or when using indefinite queries:

List<String> interests = context.findList("$.person.mother.interests");

You can use inline predicates, which will return a List of the results:

final Condition cheapBookCondition =
  (level, context) -> ! context.findList("$.store.book[?(@.price < 10)]").isEmpty();

The inline and filter predicates are not available for exceptions. Instead, you must use filter:

class FindException {
  void logException() {
    Condition throwableCondition =
      (level, ctx) ->
        ctx.findThrowable()
          .filter(e -> "test message".equals(e.getMessage()))
          .isPresent();

    logger.error(throwableCondition, "Error message", new RuntimeException("test message"));
  }
}

There are many more options available using JSONPath. You can try out the online evaluator to test out expressions.

Logger

You can use conditions in a logger, and statements will only log if the condition is met:

var loggerWithCondition = logger.withCondition(condition);

You can also build up conditions:

Logger<PresentationFieldBuilder> loggerWithAandB = logger.withCondition(conditionA).withCondition(conditionB);

Conditions are only evaluated once a level/marker check is passed, so something like

loggerWithAandB.trace("some message");

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:

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

Statement

You can also use conditions in an individual statement:

logger.info(mustHaveFoo, "Only log if foo is present");

Predicates

Conditions can also be used in predicate blocks for expensive objects.

if (logger.isInfoEnabled(condition)) {
  // only true if condition and is info  
}

Conditions will only be checked after an isEnabled check is passed -- the level (and optional marker) is always checked first, before any conditions.

A condition may also evaluate context fields that are set in a logger:

// Conditions may evaluate context
Condition cond = (level, ctx) -> ctx.findString("somename").isPresent();
boolean loggerEnabled = logger
  .withFields(fb -> 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:

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

// check evaluates context
if (loggerWithContextAndCondition.isInfoEnabled()) {
  // info statement _also_ evaluates context
  loggerWithContextAndCondition.info("some message");
}

This results in the context being evaluated both in the block and in the info statement itself, which is inefficient.

It is generally preferable to pass in a condition explicitly on the statement, as it will only evaluate once.

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

or just on the statement.

loggerWithContextAndCondition.info("some message");