package org.fenixedu.bennu.scheduler.log; import java.io.PrintWriter; import java.io.StringWriter; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Collections; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import org.fenixedu.bennu.scheduler.custom.CustomTask; import org.fenixedu.commons.stream.StreamUtils; import org.joda.time.DateTime; import com.google.common.collect.ImmutableSet; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; /** * An execution log is a record containing all the relevant information about a specific execution of a task. * * Each execution is assigned an ID that is unique across all executions of the same task. * * An execution log is immutable, meaning that different snapshots of a given execution can be taken. Newer versions of log for * this execution must be retrieved using the log repository. * * It may optionally hold the code of the task that was run, as well as the user who triggered its execution. This usage is mostly * prevalent in manually-input {@link CustomTask}s. * * @author João Carvalho (joao.pedro.carvalho@tecnico.ulisboa.pt) * */ public class ExecutionLog { private static String computeHostName() { try { return InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { return null; } } /** * Represents the current state of a task. * * Every task starts in the {@link #RUNNING} state, and when the task is complete, transitions to either the {@link #SUCCESS} * or {@link #FAILURE} states, depending of whether the task was successful or not. * * @author João Carvalho (joao.pedro.carvalho@tecnico.ulisboa.pt) */ public static enum TaskState { RUNNING, SUCCESS, FAILURE; } private final String id; private final DateTime start; private final Optional<DateTime> end; private final TaskState state; private final String taskName; private final Optional<String> stackTrace; private final Set<String> files; private final String hostname; private final Optional<String> code; private final Optional<String> user; /** * Creates a new Execution Log from the provided JSON representation. * * The provided JSON <b>must</b> have been returned by another log's {@link #json()} method, otherwise the results of invoking * this constructor are undefined. * * @param json * The JSON object containing the description of an execution log */ protected ExecutionLog(JsonObject json) { this.id = string(json, "id").get(); this.start = string(json, "start").map(DateTime::new).get(); this.end = string(json, "end").map(DateTime::new); this.state = string(json, "state").map(TaskState::valueOf).get(); this.taskName = string(json, "taskName").get(); this.stackTrace = string(json, "stackTrace"); this.files = StreamUtils.of(json.getAsJsonArray("files")).map(file -> file.getAsJsonPrimitive().getAsString()) .collect(Collectors.toSet()); this.hostname = string(json, "hostname").get(); this.code = string(json, "code"); this.user = string(json, "user"); } private static final Optional<String> string(JsonObject json, String property) { return Optional.ofNullable(json.getAsJsonPrimitive(property)).map(JsonPrimitive::getAsString); } /* * Standard constructor with all the required fields */ private ExecutionLog(String id, DateTime start, Optional<DateTime> end, TaskState state, String taskName, Optional<String> stackTrace, Set<String> files, String hostname, Optional<String> code, Optional<String> user) { super(); this.id = id; this.start = start; this.end = end; this.state = state; this.taskName = taskName; this.stackTrace = stackTrace; this.files = files; this.hostname = hostname; this.code = code; this.user = user; } /** * Creates a new execution log for the given task, automatically assigning it a randomly-generated unique identifier. * * The returned log is in the {@link TaskState#RUNNING} state, with the current time as the start time. * * @param taskName * The name of the task that was run * @return * A new execution log for the given task * @throws NullPointerException * If the task name is {@code null} */ public static ExecutionLog newExecutionFor(String taskName) { return new ExecutionLog(UUID.randomUUID().toString().replace("-", ""), DateTime.now(), Optional.empty(), TaskState.RUNNING, Objects.requireNonNull(taskName), Optional.empty(), Collections.emptySet(), computeHostName(), Optional.empty(), Optional.empty()); } /** * Creates a new execution log for the given custom task, taking note of the code that was run, and the user who ran it. * * Just like the {@link #newExecutionFor(String)} method, the returned log is assigned a randomly-generated unique identifier, * an is in the {@link TaskState#RUNNING} state, with the current time as the start time. * * @param taskName * The name of the task that was run * @param code * The code that was run * @param user * The user who ran the task * @return * A new execution log for the given task * @throws NullPointerException * If either the task name, code or user is {@code null} */ public static ExecutionLog newCustomExecution(String taskName, String code, String user) { return new ExecutionLog(UUID.randomUUID().toString().replace("-", ""), DateTime.now(), Optional.empty(), TaskState.RUNNING, Objects.requireNonNull(taskName), Optional.empty(), Collections.emptySet(), computeHostName(), Optional.of(code), Optional.of(user)); } /** * Returns the unique identifier of this execution. * * @return * The identifier of the execution */ public String getId() { return id; } /** * Returns the date in which this execution has started. * * @return * The start date for this execution */ public DateTime getStart() { return start; } /** * If this task is in the {@link TaskState#RUNNING} state, returns an empty {@link Optional}. Otherwise returns the date in * which this execution has ended. * * @return * The end date for this execution, may be empty */ public Optional<DateTime> getEnd() { return end; } /** * Returns the {@link TaskState} for this execution. * * @return * The current state of this execution */ public TaskState getState() { return state; } /** * Returns the name of the task that this execution refers to. * * @return * The name of the task being executed */ public String getTaskName() { return taskName; } /** * If the task is in the {@link TaskState#RUNNING} state, returns the stack trace of the execution thrown by the task, if one * is present. * * Otherwise, returns an empty {@link Optional}. * * @return * The stack trace of the exception thrown by the task, may be empty */ public Optional<String> getStackTrace() { return stackTrace; } /** * Returns an immutable set of all the filenames output by the task during its execution. * * @return * The filenames of all the files output by the task */ public Set<String> getFiles() { return files; } /** * Returns the canonical hostname of the machine that ran this task. * * @return * The hostname of the machine that ran the task */ public String getHostname() { return hostname; } /** * If this log refers to a {@link CustomTask}, the code that was run is returned. Otherwise, returns an empty {@link Optional} * . * * @return * The code that was run, may be empty */ public Optional<String> getCode() { return code; } /** * If this log refers to a {@link CustomTask}, the username of the user that ran the task is returned. Otherwise, returns an * empty {@link Optional}. * * @return * The user who ran the task, may be empty */ public Optional<String> getUser() { return user; } /** * Creates a new {@link ExecutionLog} based on this one, marking it as successful, ending in the moment of the method call. * * This is typically invoked upon successful task completion, so that it may be recorded in the log repository. * * @return * A copy of this log marked as successful */ public ExecutionLog withSuccess() { return new ExecutionLog(id, start, Optional.of(DateTime.now()), TaskState.SUCCESS, taskName, Optional.empty(), files, hostname, code, user); } /** * Creates a new {@link ExecutionLog} based on this one, marking it as failed, with the stack trace provided by the given * throwable. The returned log has its end date as the current date. * * This is typically invoked upon failed task completion, so that it may be recorded in the log repository. * * @param t * The exception that caused this task to fail * @return * A copy of this log marked as failure */ public ExecutionLog withError(Throwable t) { StringWriter stacktrace = new StringWriter(); try (PrintWriter writer = new PrintWriter(stacktrace)) { t.printStackTrace(writer); } return new ExecutionLog(id, start, Optional.of(DateTime.now()), TaskState.FAILURE, taskName, Optional.of(stacktrace .toString()), files, hostname, code, user); } /** * Creates a new {@link ExecutionLog} based on this one, with the given filename added to the list of files generated by the * task. * * @param filename * The name of the file to be added * @return * A copy of this log with the added file * @throws NullPointerException * If the given filename was null */ public ExecutionLog withFile(String filename) { ImmutableSet.Builder<String> builder = ImmutableSet.builder(); builder.addAll(files); builder.add(filename); return new ExecutionLog(id, start, end, state, taskName, stackTrace, builder.build(), hostname, code, user); } /** * Returns a JSON representation of this log. * * Optional slots are only present in the returned JSON if they are non-empty. * * <p> * <b>Note:</b> The JSON returned by this method can be used by log repositories to re-create instances of * {@link ExecutionLog} from their JSON representation, via the package-protected {@link #ExecutionLog(JsonObject)} * constructor. * </p> * * @return * A JSON representation of this log */ public JsonObject json() { JsonObject json = new JsonObject(); json.addProperty("id", id); json.addProperty("start", start.toString()); end.ifPresent(val -> json.addProperty("end", val.toString())); json.addProperty("state", state.name()); json.addProperty("taskName", taskName); stackTrace.ifPresent(val -> json.addProperty("stackTrace", val)); json.add("files", files.stream().map(JsonPrimitive::new).collect(StreamUtils.toJsonArray())); json.addProperty("hostname", hostname); code.ifPresent(val -> json.addProperty("code", val)); user.ifPresent(val -> json.addProperty("user", val)); return json; } /** * {@inheritDoc} */ @Override public int hashCode() { return Objects.hash(id, start, end, state, taskName, stackTrace, hostname, files, code, user); } /** * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (obj instanceof ExecutionLog) { ExecutionLog other = (ExecutionLog) obj; return Objects.equals(id, other.id) && Objects.equals(start, other.start) && Objects.equals(end, other.end) && Objects.equals(state, other.state) && Objects.equals(taskName, other.taskName) && Objects.equals(stackTrace, other.stackTrace) && Objects.equals(hostname, other.hostname) && Objects.equals(files, other.files) && Objects.equals(code, other.code) && Objects.equals(user, other.user); } return false; } }