[epilogue] Add an annotation-based logging framework for Java programs (#6584)

This commit is contained in:
Sam Carlberg
2024-07-16 20:25:43 -04:00
committed by GitHub
parent 30c7632ab8
commit 59256f0e00
51 changed files with 5147 additions and 7 deletions

View File

@@ -0,0 +1,30 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Placed on a subclass of {@code ClassSpecificLogger}. Epilogue will detect it at compile time and
* allow logging of data types compatible with the logger.
*
* <pre><code>
* {@literal @}CustomLoggerFor(VendorMotorType.class)
* class ExampleMotorLogger extends ClassSpecificLogger&lt;VendorMotorType&gt; { }
* </code></pre>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomLoggerFor {
/**
* The class or classes of objects able to be logged with the annotated logger.
*
* @return the supported data types
*/
Class<?>[] value();
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue;
import edu.wpi.first.epilogue.logging.DataLogger;
import edu.wpi.first.epilogue.logging.NTDataLogger;
import edu.wpi.first.epilogue.logging.errors.ErrorHandler;
import edu.wpi.first.epilogue.logging.errors.ErrorPrinter;
import edu.wpi.first.networktables.NetworkTableInstance;
/**
* A configuration object to be used by the generated {@code Epilogue} class to customize its
* behavior.
*/
@SuppressWarnings("checkstyle:MemberName")
public class EpilogueConfiguration {
/**
* The data logger implementation for Epilogue to use. By default, this will log data directly to
* NetworkTables. NetworkTable data can be mirrored to a log file on disk by calling {@code
* DataLogManager.start()} in your {@code robotInit} method.
*/
public DataLogger dataLogger = new NTDataLogger(NetworkTableInstance.getDefault());
/**
* The minimum importance level of data to be logged. Defaults to debug, which logs data of all
* importance levels. Any data tagged with an importance level lower than this will not be logged.
*/
public Logged.Importance minimumImportance = Logged.Importance.DEBUG;
/**
* The error handler for loggers to use if they encounter an error while logging. Defaults to
* printing an error to the standard output.
*/
public ErrorHandler errorHandler = new ErrorPrinter();
/**
* The root identifier to use for all logged data. Defaults to "Robot", but can be changed to any
* string.
*/
public String root = "Robot";
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Place this annotation on a class to automatically log every field and every public accessor
* method (methods with no arguments and return a loggable data type). Use {@link #strategy()} to
* flag a class as logging everything it can, except for those elements tagged with
* {@code @Logged(importance = NONE)}; or for logging only specific items also tagged with
* {@code @Logged}.
*
* <p>Logged fields may have any access modifier. Logged methods must be public; non-public methods
* will be ignored.
*
* <p>Epilogue can log all primitive types, arrays of primitive types (except char and short),
* Strings, arrays of Strings, sendable objects, objects with a struct serializer, and arrays of
* objects with struct serializers.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
public @interface Logged {
/**
* The name for the annotated element to be logged as. Does nothing on class-level annotations.
* Fields and methods will default to be logged using their in-code names; use this attribute to
* set it to something custom.
*
* <p>If the annotation is placed on a class, the specified name will not change logged data
* (since that uses the names of the specific usages of the class in fields and methods); however,
* it will be used to set the names of the generated logger that Logged will use to log instances
* of the class. This can be used to avoid name conflicts if you have multiple classes with the
* same name, but in different packages, and want to be able to log both.
*
* @return the name to use to log the field or method under; or the name of the generated
* class-specific logger
*/
String name() default "";
/** Opt-in or opt-out strategies for logging. */
enum Strategy {
/**
* Log everything except for those elements explicitly opted out of with the skip = true
* attribute. This is the default behavior.
*/
OPT_OUT,
/** Log only fields and methods tagged with an {@link Logged} annotation. */
OPT_IN
}
/**
* The strategy to use for logging. Only has an effect on annotations on class or interface
* declarations.
*
* @return the strategy to use to determine which fields and methods in the class to log
*/
Strategy strategy() default Strategy.OPT_OUT;
/**
* Data importance. Can be used at the class level to set the default importance for all data
* points in the class, and can be used on individual fields and methods to set a specific
* importance level overriding the class-level default.
*/
enum Importance {
/** Debug information. Useful for low-level information like raw sensor values. */
DEBUG,
/**
* Informational data. Useful for higher-level information like pose estimates or subsystem
* state.
*/
INFO,
/** Critical data that should always be present in logs. */
CRITICAL
}
/**
* The importance of the annotated data. If placed on a class or interface, this will be the
* default importance of all data within that class; this can be overridden on a per-element basis
* by annotating fields and methods with their own {@code @Logged(importance = ...)} annotation.
*
* @return the importance of the annotated element
*/
Importance importance() default Importance.DEBUG;
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A field or method annotated as {@code @NotLogged} will be ignored by Epilogue when determining
* the data to log.
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotLogged {}

View File

@@ -0,0 +1,119 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.epilogue.CustomLoggerFor;
import edu.wpi.first.epilogue.logging.errors.ErrorHandler;
import edu.wpi.first.util.sendable.Sendable;
import edu.wpi.first.util.sendable.SendableBuilder;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Base class for class-specific generated loggers. Loggers are generated at compile time by the
* Epilogue annotation processor and are used at runtime for zero-overhead data logging. Users may
* also declare custom loggers, annotated with {@link CustomLoggerFor @CustomLoggerFor}, for
* Epilogue to pull in during compile time to use for logging third party types.
*
* @param <T> the type of data supported by the logger
*/
@SuppressWarnings("unused") // Used by generated subclasses
public abstract class ClassSpecificLogger<T> {
private final Class<T> m_clazz;
// TODO: This will hold onto Sendables that are otherwise no longer referenced by a robot program.
// Determine if that's a concern
// Linked hashmap to maintain insert order
private final Map<Sendable, SendableBuilder> m_sendables = new LinkedHashMap<>();
@SuppressWarnings("PMD.RedundantFieldInitializer")
private boolean m_disabled = false;
/**
* Instantiates the logger.
*
* @param clazz the Java class of objects that can be logged
*/
protected ClassSpecificLogger(Class<T> clazz) {
this.m_clazz = clazz;
}
/**
* Updates an object's fields in a data log.
*
* @param dataLogger the logger to update
* @param object the object to update in the log
*/
protected abstract void update(DataLogger dataLogger, T object);
/**
* Attempts to update the data log. Will do nothing if the logger is {@link #disable() disabled}.
*
* @param dataLogger the logger to log data to
* @param object the data object to log
* @param errorHandler the handler to use if logging raised an exception
*/
@SuppressWarnings("PMD.AvoidCatchingGenericException")
public final void tryUpdate(DataLogger dataLogger, T object, ErrorHandler errorHandler) {
if (m_disabled) {
return;
}
try {
update(dataLogger, object);
} catch (Exception e) {
errorHandler.handle(e, this);
}
}
/**
* Checks if this logger has been disabled.
*
* @return true if this logger has been disabled by {@link #disable()}, false if not
*/
public final boolean isDisabled() {
return m_disabled;
}
/** Disables this logger. Any log calls made while disabled will be ignored. */
public final void disable() {
m_disabled = true;
}
/** Reenables this logger after being disabled. Has no effect if the logger is already enabled. */
public final void reenable() {
m_disabled = false;
}
/**
* Gets the type of the data this logger accepts.
*
* @return the logged data type
*/
public final Class<T> getLoggedType() {
return m_clazz;
}
/**
* Logs a sendable type.
*
* @param dataLogger the logger to log data into
* @param sendable the sendable object to log
*/
protected void logSendable(DataLogger dataLogger, Sendable sendable) {
if (sendable == null) {
return;
}
var builder =
m_sendables.computeIfAbsent(
sendable,
s -> {
var b = new LogBackedSendableBuilder(dataLogger);
s.initSendable(b);
return b;
});
builder.update();
}
}

View File

@@ -0,0 +1,229 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.units.Measure;
import edu.wpi.first.units.Unit;
import edu.wpi.first.util.struct.Struct;
import java.util.Collection;
/** A data logger is a generic interface for logging discrete data points. */
public interface DataLogger {
/**
* Creates a data logger that logs to multiple backends at once. Data reads will still only occur
* once; data is passed to all composed loggers at once.
*
* @param loggers the loggers to compose together
* @return the multi logger
*/
static DataLogger multi(DataLogger... loggers) {
return new MultiLogger(loggers);
}
/**
* Creates a lazy version of this logger. A lazy logger will only log data to a field when its
* value changes, which can help keep file size and bandwidth usage in check. However, there is an
* additional CPU and memory overhead associated with tracking the current value of every logged
* entry. The most surefire way to reduce CPU and memory usage associated with logging is to log
* fewer things - which can be done by opting out of logging unnecessary data or increasing the
* minimum logged importance level in the Epilogue configuration.
*
* @return the lazy logger
*/
default DataLogger lazy() {
return new LazyLogger(this);
}
/**
* Gets a logger that can be used to log nested data underneath a specific path.
*
* @param path the path to use for logging nested data under
* @return the sub logger
*/
DataLogger getSubLogger(String path);
/**
* Logs a 32-bit integer data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, int value);
/**
* Logs a 64-bit integer data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, long value);
/**
* Logs a 32-bit floating point data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, float value);
/**
* Logs a 64-bit floating point data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, double value);
/**
* Logs a boolean data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, boolean value);
/**
* Logs a raw byte array data point. <strong>NOTE:</strong> serializable data should be logged
* using {@link #log(String, Object, Struct)}.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, byte[] value);
/**
* Logs a 32-bit integer array data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, int[] value);
/**
* Logs a 64-bit integer array data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, long[] value);
/**
* Logs a 32-bit floating point array data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, float[] value);
/**
* Logs a 64-bit floating point array data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, double[] value);
/**
* Logs a boolean array data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, boolean[] value);
/**
* Logs a text data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, String value);
/**
* Logs a string array data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
void log(String identifier, String[] value);
/**
* Logs a collection of strings data point.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
*/
default void log(String identifier, Collection<String> value) {
log(identifier, value.toArray(String[]::new));
}
/**
* Logs a struct-serializable object.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
* @param struct the struct to use to serialize the data
* @param <S> the serializable type
*/
<S> void log(String identifier, S value, Struct<S> struct);
/**
* Logs an array of struct-serializable objects.
*
* @param identifier the identifier of the data point
* @param value the value of the data point
* @param struct the struct to use to serialize the objects
* @param <S> the serializable type
*/
<S> void log(String identifier, S[] value, Struct<S> struct);
/**
* Logs a collection of struct-serializable objects.
*
* @param identifier the identifier of the data
* @param value the collection of objects to log
* @param struct the struct to use to serialize the objects
* @param <S> the serializable type
*/
default <S> void log(String identifier, Collection<S> value, Struct<S> struct) {
@SuppressWarnings("unchecked")
S[] array = (S[]) value.toArray(Object[]::new);
log(identifier, array, struct);
}
/**
* Logs a measurement's value in terms of its base unit.
*
* @param identifier the identifier of the data field
* @param value the new value of the data field
*/
default void log(String identifier, Measure<?> value) {
log(identifier, value.baseUnitMagnitude());
}
/**
* Logs a measurement's value in terms of another unit.
*
* @param identifier the identifier of the data field
* @param value the new value of the data field
* @param unit the unit to log the measurement in
* @param <U> the dimension of the unit
*/
default <U extends Unit<U>> void log(String identifier, Measure<U> value, U unit) {
log(identifier, value.in(unit));
}
/**
* Logs an enum value. The value will appear as a string entry using the name of the enum.
*
* @param identifier the identifier of the data field
* @param value the new value of the data field
*/
default void log(String identifier, Enum<?> value) {
log(identifier, value.name());
}
// TODO: Add default methods to support common no-struct no-sendable types like joysticks?
}

View File

@@ -0,0 +1,144 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import static edu.wpi.first.util.ErrorMessages.requireNonNullParam;
import edu.wpi.first.util.datalog.BooleanArrayLogEntry;
import edu.wpi.first.util.datalog.BooleanLogEntry;
import edu.wpi.first.util.datalog.DataLog;
import edu.wpi.first.util.datalog.DataLogEntry;
import edu.wpi.first.util.datalog.DoubleArrayLogEntry;
import edu.wpi.first.util.datalog.DoubleLogEntry;
import edu.wpi.first.util.datalog.FloatArrayLogEntry;
import edu.wpi.first.util.datalog.FloatLogEntry;
import edu.wpi.first.util.datalog.IntegerArrayLogEntry;
import edu.wpi.first.util.datalog.IntegerLogEntry;
import edu.wpi.first.util.datalog.RawLogEntry;
import edu.wpi.first.util.datalog.StringArrayLogEntry;
import edu.wpi.first.util.datalog.StringLogEntry;
import edu.wpi.first.util.datalog.StructArrayLogEntry;
import edu.wpi.first.util.datalog.StructLogEntry;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;
/** A data logger implementation that saves information to a WPILib {@link DataLog} file on disk. */
public class FileLogger implements DataLogger {
private final DataLog m_dataLog;
private final Map<String, DataLogEntry> m_entries = new HashMap<>();
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
/**
* Creates a new file logger.
*
* @param dataLog the data log to save data to
*/
public FileLogger(DataLog dataLog) {
this.m_dataLog = requireNonNullParam(dataLog, "dataLog", "FileLogger");
}
@Override
public DataLogger getSubLogger(String path) {
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
}
@SuppressWarnings("unchecked")
private <E extends DataLogEntry> E getEntry(
String identifier, BiFunction<DataLog, String, ? extends E> ctor) {
if (m_entries.get(identifier) != null) {
return (E) m_entries.get(identifier);
}
var entry = ctor.apply(m_dataLog, identifier);
m_entries.put(identifier, entry);
return entry;
}
@Override
public void log(String identifier, int value) {
getEntry(identifier, IntegerLogEntry::new).append(value);
}
@Override
public void log(String identifier, long value) {
getEntry(identifier, IntegerLogEntry::new).append(value);
}
@Override
public void log(String identifier, float value) {
getEntry(identifier, FloatLogEntry::new).append(value);
}
@Override
public void log(String identifier, double value) {
getEntry(identifier, DoubleLogEntry::new).append(value);
}
@Override
public void log(String identifier, boolean value) {
getEntry(identifier, BooleanLogEntry::new).append(value);
}
@Override
public void log(String identifier, byte[] value) {
getEntry(identifier, RawLogEntry::new).append(value);
}
@Override
@SuppressWarnings("PMD.UnnecessaryCastRule")
public void log(String identifier, int[] value) {
long[] widened = new long[value.length];
for (int i = 0; i < value.length; i++) {
widened[i] = (long) value[i];
}
getEntry(identifier, IntegerArrayLogEntry::new).append(widened);
}
@Override
public void log(String identifier, long[] value) {
getEntry(identifier, IntegerArrayLogEntry::new).append(value);
}
@Override
public void log(String identifier, float[] value) {
getEntry(identifier, FloatArrayLogEntry::new).append(value);
}
@Override
public void log(String identifier, double[] value) {
getEntry(identifier, DoubleArrayLogEntry::new).append(value);
}
@Override
public void log(String identifier, boolean[] value) {
getEntry(identifier, BooleanArrayLogEntry::new).append(value);
}
@Override
public void log(String identifier, String value) {
getEntry(identifier, StringLogEntry::new).append(value);
}
@Override
public void log(String identifier, String[] value) {
getEntry(identifier, StringArrayLogEntry::new).append(value);
}
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S value, Struct<S> struct) {
m_dataLog.addSchema(struct);
getEntry(identifier, (log, k) -> StructLogEntry.create(log, k, struct)).append(value);
}
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S[] value, Struct<S> struct) {
m_dataLog.addSchema(struct);
getEntry(identifier, (log, k) -> StructArrayLogEntry.create(log, k, struct)).append(value);
}
}

View File

@@ -0,0 +1,240 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.struct.Struct;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* A data logger implementation that only logs data when it changes. Useful for keeping bandwidth
* and file sizes down. However, because it still needs to check that data has changed, it cannot
* avoid expensive sensor reads.
*/
public class LazyLogger implements DataLogger {
private final DataLogger m_logger;
// Keep a record of the most recent value written to each entry
// Note that this may duplicate a lot of data, and will box primitives.
private final Map<String, Object> m_previousValues = new HashMap<>();
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
/**
* Creates a new lazy logger wrapper around another logger.
*
* @param logger the logger to delegate to
*/
public LazyLogger(DataLogger logger) {
this.m_logger = logger;
}
@Override
public DataLogger lazy() {
// Already lazy, don't need to wrap it again
return this;
}
@Override
public DataLogger getSubLogger(String path) {
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
}
@Override
public void log(String identifier, int value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof Integer oldValue && oldValue == value) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, long value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof Long oldValue && oldValue == value) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, float value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof Float oldValue && oldValue == value) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, double value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof Double oldValue && oldValue == value) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, boolean value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof Boolean oldValue && oldValue == value) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, byte[] value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof byte[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, int[] value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof int[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, long[] value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof long[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, float[] value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof float[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, double[] value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof double[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, boolean[] value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof boolean[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, String value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof String oldValue && oldValue.equals(value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public void log(String identifier, String[] value) {
var previous = m_previousValues.get(identifier);
if (previous instanceof String[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value);
}
@Override
public <S> void log(String identifier, S value, Struct<S> struct) {
var previous = m_previousValues.get(identifier);
if (Objects.equals(previous, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value, struct);
}
@Override
public <S> void log(String identifier, S[] value, Struct<S> struct) {
var previous = m_previousValues.get(identifier);
if (previous instanceof Object[] oldValue && Arrays.equals(oldValue, value)) {
// no change
return;
}
m_previousValues.put(identifier, value);
m_logger.log(identifier, value, struct);
}
}

View File

@@ -0,0 +1,212 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.function.BooleanConsumer;
import edu.wpi.first.util.function.FloatConsumer;
import edu.wpi.first.util.function.FloatSupplier;
import edu.wpi.first.util.sendable.SendableBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
import java.util.function.DoubleSupplier;
import java.util.function.LongConsumer;
import java.util.function.LongSupplier;
import java.util.function.Supplier;
/** A sendable builder implementation that sends data to a {@link DataLogger}. */
@SuppressWarnings("PMD.CouplingBetweenObjects") // most methods simply delegate to the logger
public class LogBackedSendableBuilder implements SendableBuilder {
private final DataLogger m_logger;
private final Collection<Runnable> m_updates = new ArrayList<>();
/**
* Creates a new sendable builder that delegates writes to an underlying data logger.
*
* @param logger the data logger to write the sendable data to
*/
public LogBackedSendableBuilder(DataLogger logger) {
this.m_logger = logger;
}
@Override
public void setSmartDashboardType(String type) {
m_logger.log(".type", type);
}
@Override
public void setActuator(boolean value) {
// ignore
}
@Override
public void setSafeState(Runnable func) {
// ignore
}
@Override
public void addBooleanProperty(String key, BooleanSupplier getter, BooleanConsumer setter) {
m_updates.add(() -> m_logger.log(key, getter.getAsBoolean()));
}
@Override
public void publishConstBoolean(String key, boolean value) {
m_logger.log(key, value);
}
@Override
public void addIntegerProperty(String key, LongSupplier getter, LongConsumer setter) {
m_updates.add(() -> m_logger.log(key, getter.getAsLong()));
}
@Override
public void publishConstInteger(String key, long value) {
m_logger.log(key, value);
}
@Override
public void addFloatProperty(String key, FloatSupplier getter, FloatConsumer setter) {
m_updates.add(() -> m_logger.log(key, getter.getAsFloat()));
}
@Override
public void publishConstFloat(String key, float value) {
m_logger.log(key, value);
}
@Override
public void addDoubleProperty(String key, DoubleSupplier getter, DoubleConsumer setter) {
m_updates.add(() -> m_logger.log(key, getter.getAsDouble()));
}
@Override
public void publishConstDouble(String key, double value) {
m_logger.log(key, value);
}
@Override
public void addStringProperty(String key, Supplier<String> getter, Consumer<String> setter) {
if (getter != null) {
m_updates.add(() -> m_logger.log(key, getter.get()));
}
}
@Override
public void publishConstString(String key, String value) {
m_logger.log(key, value);
}
@Override
public void addBooleanArrayProperty(
String key, Supplier<boolean[]> getter, Consumer<boolean[]> setter) {
if (getter != null) {
m_updates.add(() -> m_logger.log(key, getter.get()));
}
}
@Override
public void publishConstBooleanArray(String key, boolean[] value) {
m_logger.log(key, value);
}
@Override
public void addIntegerArrayProperty(
String key, Supplier<long[]> getter, Consumer<long[]> setter) {
if (getter != null) {
m_updates.add(() -> m_logger.log(key, getter.get()));
}
}
@Override
public void publishConstIntegerArray(String key, long[] value) {
m_logger.log(key, value);
}
@Override
public void addFloatArrayProperty(
String key, Supplier<float[]> getter, Consumer<float[]> setter) {
if (getter != null) {
m_updates.add(() -> m_logger.log(key, getter.get()));
}
}
@Override
public void publishConstFloatArray(String key, float[] value) {
m_logger.log(key, value);
}
@Override
public void addDoubleArrayProperty(
String key, Supplier<double[]> getter, Consumer<double[]> setter) {
if (getter != null) {
m_updates.add(() -> m_logger.log(key, getter.get()));
}
}
@Override
public void publishConstDoubleArray(String key, double[] value) {
m_logger.log(key, value);
}
@Override
public void addStringArrayProperty(
String key, Supplier<String[]> getter, Consumer<String[]> setter) {
if (getter != null) {
m_updates.add(() -> m_logger.log(key, getter.get()));
}
}
@Override
public void publishConstStringArray(String key, String[] value) {
m_logger.log(key, value);
}
@Override
public void addRawProperty(
String key, String typeString, Supplier<byte[]> getter, Consumer<byte[]> setter) {
if (getter != null) {
m_updates.add(() -> m_logger.log(key, getter.get()));
}
}
@Override
public void publishConstRaw(String key, String typeString, byte[] value) {
m_logger.log(key, value);
}
@Override
public BackendKind getBackendKind() {
return BackendKind.kUnknown;
}
@Override
public boolean isPublished() {
return true;
}
@Override
public void update() {
for (Runnable update : m_updates) {
update.run();
}
}
@Override
public void clearProperties() {
m_updates.clear();
}
@Override
public void addCloseable(AutoCloseable closeable) {
// Ignore
}
@Override
public void close() throws Exception {
clearProperties();
}
}

View File

@@ -0,0 +1,134 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A data logger implementation that delegates to other loggers. Helpful for simultaneous logging to
* multiple data stores at once.
*/
public class MultiLogger implements DataLogger {
private final List<DataLogger> m_loggers;
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
// Use DataLogger.multi(...) instead of instantiation directly
MultiLogger(DataLogger... loggers) {
this.m_loggers = List.of(loggers);
}
@Override
public DataLogger getSubLogger(String path) {
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
}
@Override
public void log(String identifier, int value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, long value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, float value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, double value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, boolean value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, byte[] value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, int[] value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, long[] value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, float[] value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, double[] value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, boolean[] value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, String value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public void log(String identifier, String[] value) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value);
}
}
@Override
public <S> void log(String identifier, S value, Struct<S> struct) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value, struct);
}
}
@Override
public <S> void log(String identifier, S[] value, Struct<S> struct) {
for (DataLogger logger : m_loggers) {
logger.log(identifier, value, struct);
}
}
}

View File

@@ -0,0 +1,167 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.networktables.BooleanArrayPublisher;
import edu.wpi.first.networktables.BooleanPublisher;
import edu.wpi.first.networktables.DoubleArrayPublisher;
import edu.wpi.first.networktables.DoublePublisher;
import edu.wpi.first.networktables.FloatArrayPublisher;
import edu.wpi.first.networktables.FloatPublisher;
import edu.wpi.first.networktables.IntegerArrayPublisher;
import edu.wpi.first.networktables.IntegerPublisher;
import edu.wpi.first.networktables.NetworkTableInstance;
import edu.wpi.first.networktables.Publisher;
import edu.wpi.first.networktables.RawPublisher;
import edu.wpi.first.networktables.StringArrayPublisher;
import edu.wpi.first.networktables.StringPublisher;
import edu.wpi.first.networktables.StructArrayPublisher;
import edu.wpi.first.networktables.StructPublisher;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.Map;
/**
* A data logger implementation that sends data over network tables. Be careful when using this,
* since sending too much data may cause bandwidth or CPU starvation.
*/
public class NTDataLogger implements DataLogger {
private final NetworkTableInstance m_nt;
private final Map<String, Publisher> m_publishers = new HashMap<>();
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
/**
* Creates a data logger that sends information to NetworkTables.
*
* @param nt the NetworkTable instance to use to send data to
*/
public NTDataLogger(NetworkTableInstance nt) {
this.m_nt = nt;
}
@Override
public DataLogger getSubLogger(String path) {
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
}
@Override
public void log(String identifier, int value) {
((IntegerPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, long value) {
((IntegerPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, float value) {
((FloatPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, double value) {
((DoublePublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, boolean value) {
((BooleanPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, byte[] value) {
((RawPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getRawTopic(k).publish("raw")))
.set(value);
}
@Override
@SuppressWarnings("PMD.UnnecessaryCastRule")
public void log(String identifier, int[] value) {
// NT backend only supports int64[], so we have to manually widen to 64 bits before sending
long[] widened = new long[value.length];
for (int i = 0; i < value.length; i++) {
widened[i] = (long) value[i];
}
((IntegerArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
.set(widened);
}
@Override
public void log(String identifier, long[] value) {
((IntegerArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getIntegerArrayTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, float[] value) {
((FloatArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getFloatArrayTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, double[] value) {
((DoubleArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getDoubleArrayTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, boolean[] value) {
((BooleanArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getBooleanArrayTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, String value) {
((StringPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringTopic(k).publish()))
.set(value);
}
@Override
public void log(String identifier, String[] value) {
((StringArrayPublisher)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStringArrayTopic(k).publish()))
.set(value);
}
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S value, Struct<S> struct) {
m_nt.addSchema(struct);
((StructPublisher<S>)
m_publishers.computeIfAbsent(identifier, k -> m_nt.getStructTopic(k, struct).publish()))
.set(value);
}
@Override
@SuppressWarnings("unchecked")
public <S> void log(String identifier, S[] value, Struct<S> struct) {
m_nt.addSchema(struct);
((StructArrayPublisher<S>)
m_publishers.computeIfAbsent(
identifier, k -> m_nt.getStructArrayTopic(k, struct).publish()))
.set(value);
}
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.struct.Struct;
/** Null data logger implementation that logs nothing. */
public class NullLogger implements DataLogger {
@Override
public DataLogger getSubLogger(String path) {
// Since a sublogger would still log nothing and has no state, we can just return the same
// null-logging implementation
return this;
}
@Override
public void log(String identifier, int value) {}
@Override
public void log(String identifier, long value) {}
@Override
public void log(String identifier, float value) {}
@Override
public void log(String identifier, double value) {}
@Override
public void log(String identifier, boolean value) {}
@Override
public void log(String identifier, byte[] value) {}
@Override
public void log(String identifier, int[] value) {}
@Override
public void log(String identifier, long[] value) {}
@Override
public void log(String identifier, float[] value) {}
@Override
public void log(String identifier, double[] value) {}
@Override
public void log(String identifier, boolean[] value) {}
@Override
public void log(String identifier, String value) {}
@Override
public void log(String identifier, String[] value) {}
@Override
public <S> void log(String identifier, S value, Struct<S> struct) {}
@Override
public <S> void log(String identifier, S[] value, Struct<S> struct) {}
}

View File

@@ -0,0 +1,115 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging;
import edu.wpi.first.util.struct.Struct;
import java.util.HashMap;
import java.util.Map;
/**
* A data logger that logs to an underlying logger, prepending all logged data with a specific
* prefix. Useful for logging nested data structures.
*/
public class SubLogger implements DataLogger {
private final String m_prefix;
private final DataLogger m_impl;
private final Map<String, SubLogger> m_subLoggers = new HashMap<>();
/**
* Creates a new sublogger underneath another logger.
*
* @param prefix the prefix to append to all data logged in the sublogger
* @param impl the data logger to log to
*/
public SubLogger(String prefix, DataLogger impl) {
// Add a trailing slash if not already present
if (prefix.endsWith("/")) {
this.m_prefix = prefix;
} else {
this.m_prefix = prefix + "/";
}
this.m_impl = impl;
}
@Override
public DataLogger getSubLogger(String path) {
return m_subLoggers.computeIfAbsent(path, k -> new SubLogger(k, this));
}
@Override
public void log(String identifier, int value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, long value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, float value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, double value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, boolean value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, byte[] value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, int[] value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, long[] value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, float[] value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, double[] value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, boolean[] value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, String value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public void log(String identifier, String[] value) {
m_impl.log(m_prefix + identifier, value);
}
@Override
public <S> void log(String identifier, S value, Struct<S> struct) {
m_impl.log(m_prefix + identifier, value, struct);
}
@Override
public <S> void log(String identifier, S[] value, Struct<S> struct) {
m_impl.log(m_prefix + identifier, value, struct);
}
}

View File

@@ -0,0 +1,22 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging.errors;
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
/**
* An error handler implementation that will throw an exception if logging raised an exception. This
* is useful when running code in simulation or in JUnit tests to quickly identify errors in your
* code.
*/
public class CrashOnError implements ErrorHandler {
@Override
public void handle(Throwable exception, ClassSpecificLogger<?> logger) {
throw new RuntimeException(
"[EPILOGUE] An error occurred while logging an instance of "
+ logger.getLoggedType().getName(),
exception);
}
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging.errors;
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
/**
* An error handler is used by the Logged framework to catch and process any errors that occur
* during the logging process. Different handlers can be used in different operating modes, such as
* crashing in simulation to identify errors before they make it to a robot, or automatically
* disabling loggers if they encounter too many errors on the field to let the robot keep running
* while playing a match.
*/
@FunctionalInterface
public interface ErrorHandler {
/**
* Handles an exception that arose while logging.
*
* @param exception the exception that occurred
* @param logger the logger that was being processed that caused the error to occur
*/
void handle(Throwable exception, ClassSpecificLogger<?> logger);
/**
* Creates an error handler that will immediately re-throw an exception and cause robot code to
* exit. This is particularly useful when running in simulation or JUnit tests to identify errors
* quickly and safely.
*
* @return the error handler
*/
static ErrorHandler crashOnError() {
return new CrashOnError();
}
/**
* Creates an error handler that will print error messages to the console output, but otherwise
* allow logging to continue in the future. This can be helpful when errors occur only rarely and
* you don't want your robot program to crash or disable future logging.
*
* @return the error handler
*/
static ErrorHandler printErrorMessages() {
return new ErrorPrinter();
}
/**
* Creates an error handler that will automatically disable a logger if it encounters too many
* errors. Only the error-prone logger(s) will be disabled; loggers that have not encountered any
* errors, or encountered fewer than the limit, will continue to be used. Disabled loggers can be
* reset by calling {@link LoggerDisabler#reset()} on the handler.
*
* @param maximumPermissibleErrors the maximum number of errors that a logger is permitted to
* encounter before being disabled.
* @return the error handler
*/
static LoggerDisabler disabling(int maximumPermissibleErrors) {
return LoggerDisabler.forLimit(maximumPermissibleErrors);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging.errors;
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
/** An error handler implementation that prints error information to the console. */
public class ErrorPrinter implements ErrorHandler {
@Override
public void handle(Throwable exception, ClassSpecificLogger<?> logger) {
System.err.println(
"[EPILOGUE] An error occurred while logging an instance of "
+ logger.getLoggedType().getName()
+ ": "
+ exception.getMessage());
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.logging.errors;
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
import java.util.HashMap;
import java.util.Map;
/**
* An error handler that disables loggers after too many exceptions are raised. Useful when playing
* in matches, where data logging is less important than reliable control. Setting the threshold to
* ≤0 will cause any logger that encounters an exception whilst logging to immediately be disabled.
* Setting to higher values means your program is more tolerant of errors, but takes longer to
* disable, and therefore may have more sets of partial or incomplete data and may have more CPU
* overhead due to the cost of throwing exceptions.
*/
public class LoggerDisabler implements ErrorHandler {
private final int m_threshold;
private final Map<ClassSpecificLogger<?>, Integer> m_errorCounts = new HashMap<>();
/**
* Creates a new logger-disabling error handler.
*
* @param threshold how many errors any one logger is allowed to encounter before it is disabled.
*/
public LoggerDisabler(int threshold) {
this.m_threshold = threshold;
}
/**
* Creates a disabler that kicks in after a logger raises more than {@code threshold} exceptions.
*
* @param threshold the threshold value for the maximum number of exceptions loggers are permitted
* to encounter before they are disabled
* @return the disabler
*/
public static LoggerDisabler forLimit(int threshold) {
return new LoggerDisabler(threshold);
}
@Override
public void handle(Throwable exception, ClassSpecificLogger<?> logger) {
var errorCount = m_errorCounts.getOrDefault(logger, 0) + 1;
m_errorCounts.put(logger, errorCount);
if (errorCount > m_threshold) {
logger.disable();
System.err.println(
"[EPILOGUE] Too many errors detected in "
+ logger.getClass().getName()
+ " (maximum allowed: "
+ m_threshold
+ "). The most recent error follows:");
System.err.println(exception.getMessage());
exception.printStackTrace(System.err);
}
}
/** Resets all error counts and reenables all loggers. */
public void reset() {
for (var logger : m_errorCounts.keySet()) {
// Safe. This is a no-op on loggers that are already enabled
logger.reenable();
}
m_errorCounts.clear();
}
}