Skip to content

Commit

Permalink
fix TTY logs: more robust debug & don't default to w/escape codes (#1425
Browse files Browse the repository at this point in the history
)

## switch to color opt-in

right now there was no way to opt-out of color (ANSI escape sequences
for terminal emulators to capture/interpret). Give us a mechanism to
control this now and also make it opt _in_ (so if someone's doing a
local demo and wants fancy colors they now just have to do 
`export FORCE_COLOR=1` or `FORCE_COLOR=1 ./gradlew ...`).

## use a timezone

Even if this is for someone's local machine, it's a debugging task, and
that involves head-scratching many minutes/hours/days later, and
communicating with other people _themselves_ in different timezones. So
including a timezone only helps us here.

## stderr, not stdout

this isn't an interactive CLI where stdout and sterr are distinguished,
but rather a long-running server's own log output. As such, stop using
stdout since there's no "nice" output: all output is for debugging
purposes (and thus, for example, has no need to be buffered on some
systems). this means if something fails (say in another log system in a
log-multiplexer) its _own_ fallback logs will be somewhat-plausibly
nearby _this_ monitor's logs, instead of hopelessly buffered to long
moments away from now.
  • Loading branch information
jzacsh authored Jan 22, 2025
1 parent 67528e0 commit 3eae329
Showing 1 changed file with 74 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package org.datatransferproject.launcher.monitor;

import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Throwables.getStackTraceAsString;

import java.util.UUID;
import org.datatransferproject.api.launcher.Monitor;

Expand All @@ -24,9 +27,16 @@
import java.util.function.Supplier;
import org.datatransferproject.launcher.monitor.events.EventCode;

/** Outputs monitor events to the console. Uses ANSI color codes in shells that support them. */
/**
* Outputs monitor events to the console. Uses ANSI color codes in shells that support them.
*
* <p>For pretty color output in your local TTY ensure your shell has set
* {@code FORCE_COLOR} environment variable to "1" or "true" (and that you're
* not on Windows).
*/
public class ConsoleMonitor implements Monitor {
private Level level;
private final Level minLevel;
private final boolean useAnsiColor = shouldUseColor();

public enum Level {
SEVERE(2),
Expand All @@ -45,49 +55,96 @@ public enum Level {
private static final String ANSI_RED = "\u001B[31m";
private static final String ANSI_BLUE = "\u001B[34m";

private boolean ansi;

public ConsoleMonitor(Level level) {
this.level = level;
ansi = !System.getProperty("os.name").contains("Windows");
/** Constructs a logger that drops all logs at a level below {@code minLevel}. */
public ConsoleMonitor(Level minLevel) {
this.minLevel = minLevel;
}

public void severe(Supplier<String> supplier, Object... data) {
output("SEVERE", supplier, ANSI_RED, data);
}

public void info(Supplier<String> supplier, Object... data) {
if (Level.INFO.value < level.value) {
if (Level.INFO.value < minLevel.value) {
return;
}
output("INFO", supplier, ANSI_BLUE, data);
}

public void debug(Supplier<String> supplier, Object... data) {
if (Level.DEBUG.value < level.value) {
if (Level.DEBUG.value < minLevel.value) {
return;
}
output("DEBUG", supplier, ANSI_BLACK, data);
}

private void output(String level, Supplier<String> supplier, String color, Object... data) {
color = ansi ? color : "";
String reset = ansi ? ANSI_RESET : "";
StringBuilder builder = new StringBuilder();
if (useAnsiColor) {
builder.append(color);
}

builder.append(level);
builder.append(" ");

// ISO, because obvz (sortable, standard), and offset because this may be
// shared/discussed/debugged outside someone's console, at which point a
// vague clock time without its timezone is useless.
builder.append(ZonedDateTime.now(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));

builder.append(" ");
builder.append(supplier.get());

if (useAnsiColor) {
builder.append(ANSI_RESET);
}

String time = ZonedDateTime.now(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println(color + level + " " + time + " " + supplier.get() + reset);
if (data != null) {
for (Object datum : data) {
if (datum instanceof Throwable) {
((Throwable) datum).printStackTrace(System.out);
builder.append(getStackTraceAsString((Throwable) datum));
} else if (datum instanceof UUID) {
System.out.println("JobId: " + ((UUID)datum).toString());
builder.append("JobId: ");
builder.append(((UUID) datum).toString());
} else if (datum instanceof EventCode) {
System.out.println("EventCode: " + datum.toString());
builder.append("EventCode: ");
builder.append(((EventCode) datum).toString());
} else if (datum != null) {
System.out.println(datum);
builder.append(datum);
}
}
}

// Write to standard error, as these are debug logs for which any buffering
// won't help us at all.
System.err.format(builder.toString());
}

/**
* Whether we think we're attached to a human's TTY that wants pretty colors,
* otherwise this is a log file someone may be hunting/analyzing later so
* ASCII-escape codes for color will just be annoying.
*/
private static final boolean shouldUseColor() {
try {
final String osName = System.getProperty("os.name");
return getEnv("FORCE_COLOR") && !getEnv("NO_COLOR") &&
!osName.contains("Windows");
} catch (Exception e) {
return false;
}
}

/** Check of boolean-esque env. variable, or false if anything goes wrong. */
private static final boolean getEnv(String envVarName) {
try {
final String rawEnvValue = System.getenv(envVarName);
if (isNullOrEmpty(rawEnvValue)) {
return false;
}
return rawEnvValue.equals("1") || rawEnvValue.equals("true");
} catch (Exception e) {
return false;
}
}
}

0 comments on commit 3eae329

Please sign in to comment.