package org.dcache.util; import com.google.common.base.CharMatcher; import com.google.common.escape.CharEscaperBuilder; import com.google.common.escape.Escaper; import com.google.common.net.InetAddresses; import org.slf4j.Logger; import javax.security.auth.Subject; import java.net.InetSocketAddress; import java.security.Principal; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.function.Function; import org.dcache.auth.GidPrincipal; import org.dcache.auth.UidPrincipal; import static com.google.common.base.Preconditions.checkState; /** * Builder implementing the NetLogger format. * * The log format was originally documented as a CEDPS best practice recommendation, * however CEDPS no longer exists. A more current description of the format can * be found at https://docs.google.com/document/d/1oeW_l_YgQbR-C_7R2cKl6eYBT5N4WSMbvz0AT6hYDvA * * The NetLogger project can be found at http://netlogger.lbl.gov */ public class NetLoggerBuilder { private static final DateTimeFormatter TS_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); private final StringBuilder s = new StringBuilder(256); private boolean omitNullValues; private Level level; private Logger logger; private static final Escaper AS_QUOTED_VALUE = new CharEscaperBuilder(). addEscape('\\', "\\\\"). addEscape('\"', "\\\""). addEscape('\n', "\\n"). addEscape('\r', "\\r"). toEscaper(); private static final CharMatcher NEEDS_QUOTING = CharMatcher.anyOf(" \"\n\r"); public enum Level { ERROR, WARN, INFO, DEBUG, TRACE } private static StringBuilder appendSubject(StringBuilder sb, Subject subject) { if (subject == null) { return sb.append("unknown"); } Long uid = null; Long gid = null; for (Principal principal : subject.getPrincipals()) { if (principal instanceof UidPrincipal) { if (((UidPrincipal) principal).getUid() == 0) { return sb.append("root"); } uid = ((UidPrincipal) principal).getUid(); } else if (principal instanceof GidPrincipal) { if (((GidPrincipal) principal).isPrimaryGroup()) { gid = ((GidPrincipal) principal).getGid(); } } } if (uid == null) { return sb.append("nobody"); } sb.append(uid).append(':'); if (gid != null) { sb.append(gid); } for (Principal principal : subject.getPrincipals()) { if (principal instanceof GidPrincipal && !((GidPrincipal) principal).isPrimaryGroup()) { sb.append(',').append(((GidPrincipal) principal).getGid()); } } return sb; } public static CharSequence describeSubject(Subject subject) { if (subject == null) { return null; } else { return appendSubject(new StringBuilder(), subject); } } private String getTimestamp() { return ZonedDateTime.now().format(TS_FORMAT); } public NetLoggerBuilder(String event) { s.append("ts=").append(getTimestamp()).append(' '); s.append("event=").append(event); } public NetLoggerBuilder(Level level, String event) { this.level = level; s.append("level=").append(level).append(' '); s.append("ts=").append(getTimestamp()).append(' '); s.append("event=").append(event); } public NetLoggerBuilder omitNullValues() { omitNullValues = true; return this; } public NetLoggerBuilder onLogger(Logger logger) { this.logger = logger; return this; } /** * Add a key-value pair. If {@literal value} is such that the resulting * output is somehow ambiguous (e.g., containing a space) then the value * is escaped and placed in quotes, otherwise the value is appended * directly after the '=' sign. * <p> * A null value is handled in one of two ways: by default, a null value is * equivalent to the empty string; however, if omitNullValues is specified * then this method does nothing when value is null. */ public NetLoggerBuilder add(String name, Object value) { if (!omitNullValues || value != null) { s.append(' ').append(name).append('='); if (value != null) { String stringValue = value.toString(); if (NEEDS_QUOTING.matchesAnyOf(stringValue)) { s.append('"').append(AS_QUOTED_VALUE.escape(stringValue)).append('"'); } else { s.append(stringValue); } } } return this; } /** * Add the value of an array if it contains a single item. An empty array * and an array with more than one item are treated as if the array is null. */ public NetLoggerBuilder addSingleValue(String name, Object[] array) { return add(name, array != null && array.length == 1 ? array [0] : null); } /** * Add the mapped value of an array if it contains a single item. An empty * array, an array with a single null item, array with a single non-null * item that maps to a null value, or an array with more than one item is * treated as if the array is null. */ public <A> NetLoggerBuilder addSingleValue(String name, A[] array, Function<A,?> toDisplayedValue) { return add(name, array != null && array.length == 1 && array [0] != null ? toDisplayedValue.apply(array [0]) : null); } /** * Add the single value of an array. The array is obtained by applying the * {@literal toArray} function to {@literal source}. If source is null then * the value is treated as if the array was null. */ public <U,A> NetLoggerBuilder addSingleValue(String name, U source, Function<U,A[]> toArray, Function<A,?> toDisplayedValue) { return addSingleValue(name, source == null ? null : toArray.apply(source), toDisplayedValue); } /** * Add the value of an array if it contains a single item. The array is * obtained from {@literal source} by applying the {@literal toArray} * function. A null source it treated as if the array is null. */ public <U,A> NetLoggerBuilder addSingleValue(String name, U source, Function<U,A[]> toArray) { return addSingleValue(name, source == null ? null : toArray.apply(source)); } /** * Add a key-value pair that describes an identity. The value is either * a single word ({@literal unknown}, {@literal root} or {@literal nobody}) * or could be the uid and a list of gid(s) of this user * ({@literal <uid>:<gid>[,<gid>...]}). */ public NetLoggerBuilder add(String name, Subject subject) { if (!omitNullValues || subject != null) { s.append(' ').append(name).append('='); appendSubject(s, subject); } return this; } /** * Add a key-value pair that describes an socket address. No attempt is * made to resolve the IP address and the value is recorded as * {@literal <addr>:<port>}. If the supplied value is null and * {@link #omitNullValues} has not been called then {@literal unknown} is * recorded. */ public NetLoggerBuilder add(String name, InetSocketAddress sock) { if (!omitNullValues || sock != null) { s.append(' ').append(name).append('='); if (sock != null) { s.append(InetAddresses.toUriString(sock.getAddress())).append(':').append(sock.getPort()); } } return this; } /** * Add a key-value pair. If the value is not null then value's string value * is escaped and written in quotes. * <p> * A null value is handled in one of two ways: by default, a null value is * equivalent to the empty string; however, if omitNullValues is specified * then this method does nothing when value is null. */ public NetLoggerBuilder addInQuotes(String name, Object value) { if (!omitNullValues || value != null) { s.append(' ').append(name).append('='); if (value != null) { s.append('"').append(AS_QUOTED_VALUE.escape(value.toString())).append('"'); } } return this; } public NetLoggerBuilder add(String name, boolean value) { return add(name, String.valueOf(value)); } public NetLoggerBuilder add(String name, char value) { return add(name, String.valueOf(value)); } public NetLoggerBuilder add(String name, double value) { return add(name, String.valueOf(value)); } public NetLoggerBuilder add(String name, float value) { return add(name, String.valueOf(value)); } public NetLoggerBuilder add(String name, int value) { return add(name, String.valueOf(value)); } public NetLoggerBuilder add(String name, long value) { return add(name, String.valueOf(value)); } @Override public String toString() { return s.toString(); } public void toLogger(Logger logger) { checkState(level != null, "Cannot log to logger without a level."); String line = toString(); switch (level) { case ERROR: logger.error(line); break; case WARN: logger.warn(line); break; case INFO: logger.info(line); break; case DEBUG: logger.debug(line); break; case TRACE: logger.trace(line); break; } } public void log() { checkState(logger != null, "can't log without logger"); this.toLogger(logger); } }