From 98a4548232afa1fbfafd6e9e79c74fbdb9b8496f Mon Sep 17 00:00:00 2001 From: mj Date: Thu, 4 Nov 2021 23:26:28 +0100 Subject: [PATCH] Bug fixing - deadlock fixed. Version 0.2.0 * Deadlock fixed. * Labels - correcting labels to be compliant with Grafana Loki. * LogController - possibility to send logs before stop. * LogController - possibility to interrupt worker thread. * LogSenderSettings - possibility to set connection timeout. * LogMonitor - on worker thread exit event added. * Documentation updated. --- README.md | 32 +-- build.gradle | 2 +- .../pl/mjaron/tinyloki/ErrorLogMonitor.java | 12 ++ .../pl/mjaron/tinyloki/ILogCollector.java | 14 +- .../java/pl/mjaron/tinyloki/ILogMonitor.java | 23 +- .../java/pl/mjaron/tinyloki/ILogStream.java | 5 +- .../pl/mjaron/tinyloki/JsonLogCollector.java | 97 +++++++-- .../pl/mjaron/tinyloki/JsonLogStream.java | 67 ++++-- src/main/java/pl/mjaron/tinyloki/Labels.java | 125 ++++++++++- .../pl/mjaron/tinyloki/LogController.java | 137 ++++++++++-- .../java/pl/mjaron/tinyloki/LogSender.java | 51 +++-- .../pl/mjaron/tinyloki/LogSenderSettings.java | 105 +++++++++ .../java/pl/mjaron/tinyloki/TinyLoki.java | 71 ++++-- src/main/java/pl/mjaron/tinyloki/Utils.java | 13 ++ .../pl/mjaron/tinyloki/VerboseLogMonitor.java | 3 + .../tinyloki/third_party/Base64Coder.java | 202 ++++++++++-------- .../pl/mjaron/tinyloki/LogCollectorTest.java | 6 +- 17 files changed, 775 insertions(+), 190 deletions(-) diff --git a/README.md b/README.md index 27f6aaa..f369cb3 100644 --- a/README.md +++ b/README.md @@ -20,30 +20,35 @@ import pl.mjaron.tinyloki.*; public class Sample { public static void main(String[] args) { - + // Initialize log controller instance with URL. // Usually more than one instance in application doesn't make sense. // Give Basic Authentication credentials or nulls. // LogController owns separate thread which sends logs periodically. LogController logController = TinyLoki.createAndStart( - "https://localhost/loki/api/v1/push", "user", "pass"); - + "https://localhost/loki/api/v1/push", "user", "pass"); + // Create streams. It is thread-safe. ILogStream stream = logController.createStream( - // Define stream labels... - TinyLoki.l(Labels.LEVEL, Labels.INFO) - .l("host", "MyComputerName") - .l("custom-label", "custom-value") + // Define stream labels... + TinyLoki.l(Labels.LEVEL, Labels.INFO) + .l("host", "MyComputerName") + .l("customLabel", "custom_value") + // Label names should start with letter + // and contain letters, digits and '_' only. + // Bad characters will be replaced by '_'. + // If first character is bad, it will be replaced by 'A'. ); - + // ... new streams and other logs here (thread-safe). stream.log("Hello world."); - + // Optionally flush logs before application exit. - logController.softStop().waitForStop(); + logController.softStop().hardStop(); } } ``` + ## Integration ### Maven Central @@ -52,19 +57,22 @@ public class Sample { ```gradle dependencies { - implementation 'io.github.mjfryc:mjaron-tinyloki-java:0.1.22' + implementation 'io.github.mjfryc:mjaron-tinyloki-java:0.2.0' } ``` + ### GitHub Packages Click the [Packages section](https://github.com/mjfryc?tab=packages&repo_name=mjaron-tinyloki-java) on the right. ### Download directly + 1. Click the [Packages section](https://github.com/mjfryc?tab=packages&repo_name=mjaron-tinyloki-java) on the right. 2. Find and download jar package from files list to e.g. `your_project_root/libs` dir. 3. Add this jar to project dependencies in build.gradle, e.g: + ```gradle dependencies { - implementation files(project.rootDir.absolutePath + '/libs/mjaron-tinyloki-java-0.1.22.jar') + implementation files(project.rootDir.absolutePath + '/libs/mjaron-tinyloki-java-0.2.0.jar') } ``` diff --git a/build.gradle b/build.gradle index 53aff69..adde07e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'io.github.mjfryc' -version '0.1.22' +version '0.2.0' repositories { mavenCentral() diff --git a/src/main/java/pl/mjaron/tinyloki/ErrorLogMonitor.java b/src/main/java/pl/mjaron/tinyloki/ErrorLogMonitor.java index 537d494..44990d2 100644 --- a/src/main/java/pl/mjaron/tinyloki/ErrorLogMonitor.java +++ b/src/main/java/pl/mjaron/tinyloki/ErrorLogMonitor.java @@ -1,5 +1,8 @@ package pl.mjaron.tinyloki; +/** + * Prints only error messages. + */ public class ErrorLogMonitor implements ILogMonitor { @Override public void send(final byte[] message) { @@ -18,4 +21,13 @@ public void sendErr(final int status, final String message) { public void onException(Exception exception) { exception.printStackTrace(); } + + @Override + public void onWorkerThreadExit(final boolean isSoft) { + if (isSoft) { + System.out.println("Worker thread exited correctly."); + } else { + System.err.println("Worker thread exited by interrupting."); + } + } } diff --git a/src/main/java/pl/mjaron/tinyloki/ILogCollector.java b/src/main/java/pl/mjaron/tinyloki/ILogCollector.java index 1b10420..4154859 100644 --- a/src/main/java/pl/mjaron/tinyloki/ILogCollector.java +++ b/src/main/java/pl/mjaron/tinyloki/ILogCollector.java @@ -9,6 +9,7 @@ public interface ILogCollector { /** * Creates a new stream. + * * @param labels Unique set of labels. * @return New stream instance. */ @@ -16,21 +17,24 @@ public interface ILogCollector { /** * Gets data from streams and clears streams state. - * @return Encoded content of streams. + * + * @return Encoded content of streams or null if there is no new logs to send. */ byte[] collect(); /** - * HTTP content type describing data type of collect() result. - * @return + * HTTP Content-Type describing data type of collect() result. + * + * @return HTTP Content-Type header value. */ String contentType(); /** * Stop thread until a new log will occur. + * * @param timeout Time in milliseconds. - * @return True if any logs has occurred in given time. + * @return Count of logs in given time. It may not be exact count of logs and depends on implementation. * @throws InterruptedException When given thread has been interrupted. */ - boolean waitForLogs(final long timeout) throws InterruptedException; + int waitForLogs(final long timeout) throws InterruptedException; } diff --git a/src/main/java/pl/mjaron/tinyloki/ILogMonitor.java b/src/main/java/pl/mjaron/tinyloki/ILogMonitor.java index 8a162a6..cf2a5b9 100644 --- a/src/main/java/pl/mjaron/tinyloki/ILogMonitor.java +++ b/src/main/java/pl/mjaron/tinyloki/ILogMonitor.java @@ -5,18 +5,39 @@ */ public interface ILogMonitor { + /** + * Called before sending given data to HTTP server. + * + * @param message Data reference. + */ void send(final byte[] message); + /** + * Called on HTTP server response with good status. + * + * @param status HTTP status. + */ void sendOk(final int status); /** * Handle send HTTP response error. + * + * @param status HTTP status code. + * @param message HTTP status message. */ void sendErr(final int status, final String message); /** * Called on any exception. - * @param exception + * + * @param exception Exception reference. */ void onException(final Exception exception); + + /** + * Called when worker thread exits. + * + * @param isSoft Tells whether worker thread has exited without interrupting. + */ + void onWorkerThreadExit(final boolean isSoft); } diff --git a/src/main/java/pl/mjaron/tinyloki/ILogStream.java b/src/main/java/pl/mjaron/tinyloki/ILogStream.java index 0a3565a..01b1fb8 100644 --- a/src/main/java/pl/mjaron/tinyloki/ILogStream.java +++ b/src/main/java/pl/mjaron/tinyloki/ILogStream.java @@ -14,7 +14,8 @@ public interface ILogStream { void log(final long timestampMs, final String line); /** - * Log line with current time. + * Thread-safe log line with current time. + * * @param line Log content. */ default void log(final String line) { @@ -23,7 +24,7 @@ default void log(final String line) { /** * Release log stream, so it isn't longer managed by its log collector. - * It is not mandatory to call if log lifetime is the same as application lifetime. + * It is not mandatory to call if this stream lifetime is the same as application lifetime. */ void release(); } diff --git a/src/main/java/pl/mjaron/tinyloki/JsonLogCollector.java b/src/main/java/pl/mjaron/tinyloki/JsonLogCollector.java index 585005f..86740ff 100644 --- a/src/main/java/pl/mjaron/tinyloki/JsonLogCollector.java +++ b/src/main/java/pl/mjaron/tinyloki/JsonLogCollector.java @@ -5,11 +5,23 @@ import java.util.List; import java.util.Map; +/** + * Collects logs in a JSON format consistent with + * Loki Push API. + */ public class JsonLogCollector implements ILogCollector { private final List streams = new ArrayList<>(); - private boolean logOccurred = false; + private int logEntriesCount = 0; + private final Object logEntriesLock = new Object(); + /** + * Creates new instance of stream which will notify this collector about new logs. + * This collector will flush logs from the stream. + * + * @param labels Unique set of labels. + * @return New instance of a stream. + */ @Override synchronized public ILogStream createStream(Map labels) { JsonLogStream stream = new JsonLogStream(this, labels); @@ -17,49 +29,98 @@ synchronized public ILogStream createStream(Map labels) { return stream; } + /** + * Given stream will not be flushed by this log collector anymore, so given stream will accumulate + * all next logs causing memory leaks (if it will not be garbage collected). + * Called by {@link JsonLogStream#release()}, so there is no need to call it directly. + * + * @param stream Stream to release. + */ synchronized public void onStreamReleased(ILogStream stream) { streams.remove((JsonLogStream) stream); } + /** + * Create complete stream chunk in JSON string, stored as UTF-8 bytes. + * + * @return JSON bytes containing stream chunk. + */ @Override public byte[] collect() { - return collectAsString().getBytes(StandardCharsets.UTF_8); + final String collectedAsString = collectAsString(); + if (collectedAsString == null) { + return null; + } + return collectedAsString.getBytes(StandardCharsets.UTF_8); } + /** + * Create complete stream chunk in JSON string. + * + * @return JSON string containing stream chunk. + */ synchronized public String collectAsString() { final StringBuilder b = new StringBuilder("{\"streams\":["); boolean isFirst = true; + boolean anyStreamNotEmpty = false; for (final JsonLogStream stream : streams) { - if (isFirst) { - isFirst = false; - } else { - b.append(','); + final String streamData = stream.flush(); + if (streamData != null) { + if (isFirst) { + isFirst = false; + } else { + b.append(','); + } + b.append(streamData); + anyStreamNotEmpty = true; } - b.append(stream.flush()); } + + if (!anyStreamNotEmpty) { + return null; + } + b.append("]}"); return b.toString(); } + /** + * Used in HTTP Content-Type header. Grafana Loki will interpret content as JSON. + * + * @return String complaint with HTTP Content-Type header, telling that content is a JSON data. + */ @Override public String contentType() { return "application/json"; } - synchronized void logOccurred() { - logOccurred = true; - notify(); + /** + * Notify log collector that any log has occurred. + * Thread safe. + */ + void logOccurred() { + synchronized (logEntriesLock) { + ++logEntriesCount; + logEntriesLock.notify(); + } } + /** + * Blocking function. Waits until at least one log from any stream occurs. + * + * @param timeout Time in milliseconds. + * @return Collected logs count. + * @throws InterruptedException When this thread is interrupted during waiting. + */ @Override - public synchronized boolean waitForLogs(final long timeout) throws InterruptedException { - if (!logOccurred) { - this.wait(timeout); - } - if (logOccurred) { - logOccurred = false; - return true; + public int waitForLogs(final long timeout) throws InterruptedException { + synchronized (logEntriesLock) { + if (logEntriesCount == 0) { + logEntriesLock.wait(timeout); + } + int logEntriesCountCopy = logEntriesCount; + logEntriesCount = 0; + return logEntriesCountCopy; } - return false; } } diff --git a/src/main/java/pl/mjaron/tinyloki/JsonLogStream.java b/src/main/java/pl/mjaron/tinyloki/JsonLogStream.java index f3eac91..1bb08c8 100644 --- a/src/main/java/pl/mjaron/tinyloki/JsonLogStream.java +++ b/src/main/java/pl/mjaron/tinyloki/JsonLogStream.java @@ -2,12 +2,22 @@ import java.util.Map; +/** + * Writes logs to JSON-formatted string. + */ public class JsonLogStream implements ILogStream { - JsonLogCollector collector; - private StringBuilder b = new StringBuilder("{\"stream\":{"); - private String initialSequenceWithHeaders = null; - private boolean firstValue = true; + private final JsonLogCollector collector; + private final StringBuilder b = new StringBuilder("{\"stream\":{"); + private final String initialSequenceWithHeaders; + private int cachedLogsCount = 0; // Must be used in synchronized methods only. + /** + * Constructor of stream. It should be created by {@link JsonLogCollector}. + * + * @param collector {@link JsonLogCollector} instance which manages this stream. + * @param labels Static labels related to this stream. + * There should not be two streams with the same set of static labels. + */ public JsonLogStream(JsonLogCollector collector, final Map labels) { this.collector = collector; boolean isFirst = true; @@ -22,7 +32,7 @@ public JsonLogStream(JsonLogCollector collector, final Map label b.append('"'); b.append(':'); b.append('"'); - b.append(entry.getValue()); + Utils.escapeJsonString(b, entry.getValue()); b.append('"'); } b.append("},\"values\":["); @@ -30,18 +40,18 @@ public JsonLogStream(JsonLogCollector collector, final Map label } @Override - synchronized public void log(long timestampMs, String line) { - if (firstValue) { - firstValue = false; - } - else { - b.append(','); + public void log(long timestampMs, String line) { + synchronized (this) { + if (cachedLogsCount != 0) { + b.append(','); + } + ++cachedLogsCount; + b.append("[\""); + b.append(timestampMs); + b.append("000000\",\""); + Utils.escapeJsonString(b, line); + b.append("\"]"); } - b.append("[\""); - b.append(timestampMs); - b.append("000000\",\""); - Utils.escapeJsonString(b, line); - b.append("\"]"); collector.logOccurred(); } @@ -50,21 +60,44 @@ public void release() { collector.onStreamReleased(this); } + /** + * Appends JSON tags which closes streams array and JSON root object. + */ public void closeStreamsEntryTag() { b.append("]}"); } + /** + * Provides access to internal string builder for custom purposes. + * This method may be changed on any implementation changes. + * + * @return internal StringBuilder instance. + */ + @SuppressWarnings("unused") public StringBuilder getStringBuilder() { return b; } + /** + * Drop old data and next prepare StringBuilder to start new chunk of stream. + */ public void clear() { b.setLength(0); b.append(initialSequenceWithHeaders); - firstValue = true; + cachedLogsCount = 0; } + /** + * Provide stream data and clear old logs. + * + * @return JSON-formatted String containing single stream logs from last flush operation or from beginning of time. + * Null if this stream is empty. + */ synchronized public String flush() { + if (cachedLogsCount == 0) { // Do not flush if there is no values inside a stream. + return null; + } + closeStreamsEntryTag(); final String result = b.toString(); this.clear(); diff --git a/src/main/java/pl/mjaron/tinyloki/Labels.java b/src/main/java/pl/mjaron/tinyloki/Labels.java index 2c2cc11..989da0f 100644 --- a/src/main/java/pl/mjaron/tinyloki/Labels.java +++ b/src/main/java/pl/mjaron/tinyloki/Labels.java @@ -4,7 +4,8 @@ import java.util.TreeMap; /** - * Common label constants and its values. + * Represents label name - label value mappings. + * Contains common label constants and its values. * Log level constants are defined at: * https://grafana.com/docs/grafana/latest/packages_api/data/loglevel/ */ @@ -51,6 +52,105 @@ public class Labels { */ public static final String UNKNOWN = "unknown"; + /** + * Verifies if labelIdentifier is not null and not empty. + * + * @param labelIdentifier Label name or labelIdentifier value. + */ + public static void assertLabelIdentifierNotNullOrEmpty(final String labelIdentifier) { + if (labelIdentifier == null) { + throw new RuntimeException("Label identifier is null."); + } + + if (labelIdentifier.isEmpty()) { + throw new RuntimeException("Label identifier is empty."); + } + } + + /** + * Checks whether label contains only letters, digits or '_' and first character is letter. + * + * @param labelIdentifier Label name or label value to check. + * @throws RuntimeException when given label identifier is invalid. + */ + public static void validateLabelIdentifierOrThrow(final String labelIdentifier) { + assertLabelIdentifierNotNullOrEmpty(labelIdentifier); + + final char firstChar = labelIdentifier.charAt(0); + if (!Character.isLetter(firstChar)) { + throw new RuntimeException("Cannot validate given label identifier: [" + labelIdentifier + "]: First character is not a letter: [" + firstChar + "]."); + } + + for (int i = 1; i < labelIdentifier.length(); ++i) { + final char ch = labelIdentifier.charAt(i); + if (!Character.isLetterOrDigit(ch) && ch != '_') { + throw new RuntimeException("Cannot validate given label identifier: [" + labelIdentifier + "]: Given character is not a letter or digit: [" + ch + "]."); + } + } + } + + /** + * Checks whether label contains only letters, digits or '_' and first character is letter. + * + * @param labelIdentifier Label name or label value to check. + * @return True when given label identifier is valid. + */ + public static boolean checkLabelIdentifierWhenNotEmpty(final String labelIdentifier) { + final char firstChar = labelIdentifier.charAt(0); + if (!Character.isLetter(firstChar)) { + return false; + } + + for (int i = 1; i < labelIdentifier.length(); ++i) { + final char ch = labelIdentifier.charAt(i); + if (!Character.isLetterOrDigit(ch) && ch != '_') { + return false; + } + } + return true; + } + + /** + * Checks whether label contains only letters, digits or '_' and first character is letter. + * + * @param labelIdentifier Label name or label value to check. + * @return True when given label identifier is valid. + */ + private static boolean checkLabelIdentifier(final String labelIdentifier) { + assertLabelIdentifierNotNullOrEmpty(labelIdentifier); + return checkLabelIdentifierWhenNotEmpty(labelIdentifier); + } + + /** + * Replaces invalid characters with `_` character. + * If first character is invalid, replaces it with `A`. + * + * @param labelIdentifier Label name or value to check. + * @return Valid labelIdentifier identifier with removed wrong symbols. + * @throws RuntimeException when given labelIdentifier is null or empty. + */ + public static String prettifyLabelIdentifier(final String labelIdentifier) { + assertLabelIdentifierNotNullOrEmpty(labelIdentifier); + if (checkLabelIdentifierWhenNotEmpty(labelIdentifier)) { // If identifier is valid, do not clone valid identifier. + return labelIdentifier; + } + + char[] stringBytes = labelIdentifier.toCharArray(); + + final char firstChar = stringBytes[0]; + if (!Character.isLetter(firstChar)) { + stringBytes[0] = 'A'; + } + + for (int i = 1; i < stringBytes.length; ++i) { + final char ch = stringBytes[i]; + if (!Character.isLetterOrDigit(ch)) { + stringBytes[i] = '_'; + } + } + return new String(stringBytes); + } + /** * Internal labels container. */ @@ -65,12 +165,29 @@ public Map getMap() { /** * Add a new label and return this object. - * @param labelName Label name. - * @param labelValue Label value. + * + * @param labelName Label name. Valid label identifier starts with letter and contains only letters, digits or '_'. + * @param labelValue Label value. Valid label identifier starts with letter and contains only letters, digits or '_'. * @return This object with added label. */ public Labels l(final String labelName, final String labelValue) { - map.put(labelName, labelValue); + final String prettifiedName = prettifyLabelIdentifier(labelName); + final String prettifiedValue = prettifyLabelIdentifier(labelValue); + map.put(prettifiedName, prettifiedValue); + return this; + } + + /** + * Put a map with labels. + * Valid label identifier starts with letter and contains only letters, digits or '_'. + * + * @param map Map containing label key - value pairs. + * @return This reference. + */ + public Labels l(final Map map) { + for (Map.Entry entry : map.entrySet()) { + this.l(entry.getKey(), entry.getValue()); + } return this; } } diff --git a/src/main/java/pl/mjaron/tinyloki/LogController.java b/src/main/java/pl/mjaron/tinyloki/LogController.java index f7d94e4..a4e2fcd 100644 --- a/src/main/java/pl/mjaron/tinyloki/LogController.java +++ b/src/main/java/pl/mjaron/tinyloki/LogController.java @@ -2,17 +2,30 @@ import java.util.Map; +/** + * Organizes cooperation between collector and sender. + * Method {@link #start()} Creates worker thread which sends new logs. + */ public class LogController { private static final long LOG_WAIT_TIME = 100; - private static final long EXIT_WAIT_TIME = 200; + private static final long DEFAULT_SOFT_STOP_WAIT_TIME = 2000; + private static final long DEFAULT_HARD_STOP_WAIT_TIME = 1000; private final ILogCollector logCollector; private final LogSender logSender; private final ILogMonitor logMonitor; private Thread workerThread = null; private boolean softFinishing = false; + private boolean softExit = false; + /** + * Main constructor designed for user of this library. + * + * @param logCollector ILogCollector implementation, which is responsible for creating new streams and collecting its logs. + * @param logSender Sends logs collected by log controller. + * @param logMonitor Handles diagnostic events from whole library. + */ public LogController(final ILogCollector logCollector, final LogSender logSender, final ILogMonitor logMonitor) { this.logCollector = logCollector; this.logSender = logSender; @@ -21,17 +34,35 @@ public LogController(final ILogCollector logCollector, final LogSender logSender this.logSender.setLogMonitor(logMonitor); } + /** + * Creates new stream from log collector. + * + * @param labels Static labels. There shouldn't be many streams with the same labels combination. + * @return New stream reference. + */ + @SuppressWarnings("unused") public ILogStream createStream(final Map labels) { return logCollector.createStream(labels); } + /** + * Creates new stream from log collector. + * + * @param labels Static labels. There shouldn't be many streams with the same labels combination. + * @return New stream reference. + */ + @SuppressWarnings("unused") public ILogStream createStream(final Labels labels) { return logCollector.createStream(labels.getMap()); } + /** + * Starts worker thread which is responsible for collecting and sending logs. + * + * @return This reference. + */ public LogController start() { - //noinspection AnonymousHasLambdaAlternative - workerThread = new Thread() { + workerThread = new Thread("LogController.workerThread") { @Override public void run() { workerLoop(); @@ -41,53 +72,129 @@ public void run() { return this; } - synchronized public LogController softStop() { + /** + * Request worker thread to do last jobs. This method is non-blocking. + * + * @return This reference. + */ + @SuppressWarnings("UnusedReturnValue") + synchronized public LogController softStopAsync() { softFinishing = true; return this; } - public boolean waitForStop() { - return waitForStop(EXIT_WAIT_TIME); + /** + * Tells whether worker thread exited or not. + * + * @return True if worker thread is terminated. + */ + public boolean isHardStopped() { + //return workerThread.isAlive(); + return (workerThread.getState() == Thread.State.TERMINATED); } - public boolean waitForStop(final long timeout) { + /** + * Interrupts worker thread and tries to join for given interruptTimeout. + * + * @param interruptTimeout Timeout in milliseconds. + * @return This reference. + */ + public LogController hardStop(final long interruptTimeout) { try { - workerThread.join(timeout); - return (workerThread.getState() == Thread.State.TERMINATED); + this.softStopAsync(); + workerThread.interrupt(); + workerThread.join(interruptTimeout); } catch (InterruptedException e) { logMonitor.onException(e); - return false; } + return this; + } + + /** + * Blocking function. Request to stop worker thread by interrupting it. Waits for interruption with default timeout. + * + * @return This reference. + * @see #hardStop(long) + */ + @SuppressWarnings("UnusedReturnValue") + public LogController hardStop() { + return this.hardStop(DEFAULT_HARD_STOP_WAIT_TIME); + } + + /** + * Blocking function. Tells to worker to send logs to this time point and exit. + * + * @param softTimeout Timeout in milliseconds. + * @return This reference. + */ + public LogController softStop(final long softTimeout) { + try { + this.softStopAsync(); + workerThread.join(softTimeout); + } catch (InterruptedException e) { + logMonitor.onException(e); + } + return this; + } + + /** + * Blocking function. Soft stop with default timeout. + * + * @return This reference. + * @see #softStop(long) + */ + @SuppressWarnings("unused") + public LogController softStop() { + return this.softStop(DEFAULT_SOFT_STOP_WAIT_TIME); } + /** + * Tells if worker thread has stopped softly, doing all its work before exiting. + * + * @return True if worker thread has exited without interruption. + */ + public boolean isSoftStopped() { + synchronized (this) { + return this.softExit; + } + } + + /** + * Defines worker thread activity. + */ public void workerLoop() { - boolean doLastCheck = false; while (true) { try { + boolean doLastCheck = false; synchronized (this) { if (softFinishing) { doLastCheck = true; } } - boolean anyLogs; + int anyLogs; if (doLastCheck) { anyLogs = logCollector.waitForLogs(1); } else { anyLogs = logCollector.waitForLogs(LOG_WAIT_TIME); } - if (anyLogs) { + if (anyLogs > 0) { final byte[] logs = logCollector.collect(); if (logs != null) { logSender.send(logs); } } if (doLastCheck) { + synchronized (this) { + softExit = true; + } + logMonitor.onWorkerThreadExit(true); return; } } catch (final InterruptedException e) { + logMonitor.onException(e); + logMonitor.onWorkerThreadExit(false); return; - } - catch (final Exception e) { + } catch (final Exception e) { logMonitor.onException(e); } } diff --git a/src/main/java/pl/mjaron/tinyloki/LogSender.java b/src/main/java/pl/mjaron/tinyloki/LogSender.java index 5aabe3d..f4a1eb3 100644 --- a/src/main/java/pl/mjaron/tinyloki/LogSender.java +++ b/src/main/java/pl/mjaron/tinyloki/LogSender.java @@ -2,18 +2,25 @@ import pl.mjaron.tinyloki.third_party.Base64Coder; -import java.io.*; +import java.io.IOException; +import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.nio.charset.StandardCharsets; -//import java.util.Base64; +/** + * Implementation of sending bytes to HTTP server. + */ public class LogSender { final LogSenderSettings settings; private ILogMonitor logMonitor = null; - private URL url; + private final URL url; + /** + * Creates and configures a new LogSender object. + * + * @param settings Parameters required for sending HTTP requests. + */ public LogSender(final LogSenderSettings settings) { this.settings = settings; try { @@ -23,36 +30,55 @@ public LogSender(final LogSenderSettings settings) { } } + /** + * Getter of {@link LogSenderSettings}. + * + * @return {@link LogSenderSettings} used by this log sender. + */ public LogSenderSettings getSettings() { return settings; } + /** + * Getter of {@link ILogMonitor}. + * + * @return {@link ILogMonitor} used by this log sender. + */ + @SuppressWarnings("unused") public ILogMonitor getLogMonitor() { return logMonitor; } + /** + * Setter of {@link ILogMonitor}. + * {@link ILogMonitor} object must be set (and not a null) before sending any data with {@link #send(byte[])}. + * + * @param logMonitor {@link ILogMonitor} reference. + */ public void setLogMonitor(ILogMonitor logMonitor) { this.logMonitor = logMonitor; } + /** + * Creates connection and sends given data by HTTP request. + * Calls several {@link ILogMonitor} methods pointing what's the request data and HTTP response result. + * + * @param message Data to send in HTTP request content. + */ void send(final byte[] message) { logMonitor.send(message); - HttpURLConnection connection = null; + HttpURLConnection connection; OutputStream outputStream = null; try { connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(settings.getConnectTimeout()); connection.setRequestMethod("POST"); connection.setRequestProperty("connection", "close"); - //connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - //connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Content-Type", settings.getContentType()); connection.setRequestProperty("Content-Length", Integer.toString(message.length)); - //connection.setRequestProperty("Content-Language", "en-US"); if (settings.getUser() != null && settings.getPassword() != null) { final String authHeaderContentString = settings.getUser() + ":" + settings.getPassword(); - //final byte[] authHeaderBytes = authHeaderContentString.getBytes(StandardCharsets.UTF_8); - //Base64.getEncoder().encodeToString(); final String authHeaderEncoded = Base64Coder.encodeString(authHeaderContentString); connection.setRequestProperty("Authorization", "Basic " + authHeaderEncoded); } @@ -68,12 +94,11 @@ void send(final byte[] message) { if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HttpURLConnection.HTTP_NO_CONTENT) { final String responseMessage = connection.getResponseMessage(); logMonitor.sendErr(responseCode, responseMessage); - } - else { + } else { logMonitor.sendOk(responseCode); } } catch (IOException e) { - throw new RuntimeException("Failed to prepare connection.", e); + throw new RuntimeException("Failed to send logs.", e); } finally { if (outputStream != null) { try { diff --git a/src/main/java/pl/mjaron/tinyloki/LogSenderSettings.java b/src/main/java/pl/mjaron/tinyloki/LogSenderSettings.java index 0535c0b..7f96ffe 100644 --- a/src/main/java/pl/mjaron/tinyloki/LogSenderSettings.java +++ b/src/main/java/pl/mjaron/tinyloki/LogSenderSettings.java @@ -1,48 +1,153 @@ package pl.mjaron.tinyloki; +/** + * Used to configure {@link LogSender}. + */ +@SuppressWarnings("UnusedReturnValue") public class LogSenderSettings { + + /** + * Default timeout for log server connecting in milliseconds. + */ + public final static int DEFAULT_CONNECT_TIMEOUT = 5000; + + /** + * HTTP connection URL. + */ private String url = null; + + /** + * HTTP Basic Authentication user. + */ private String user = null; + + /** + * HTTP Basic Authentication password. + */ private String password = null; + + /** + * Complaint with HTTP Content-Type header. + */ private String contentType = "application/json"; + /** + * Default value of connecting timeout. + * User can change connecting timeout by calling {@link #setConnectTimeout(int)}. + */ + private int connectTimeout = DEFAULT_CONNECT_TIMEOUT; + + /** + * Creates a new instance of {@link LogSenderSettings}. + * It may be used instead of constructor. + * + * @return New instance of {@link LogSenderSettings}. + */ public static LogSenderSettings create() { return new LogSenderSettings(); } + /** + * Getter of HTTP URL used to connect to Grafana Loki server. + * This URL's path should end with `/loki/api/v1/push`. + * + * @return HTTP URL used to connect to Grafana Loki server. + */ public String getUrl() { return url; } + /** + * Setter of HTTP URL used to connect to Grafana Loki server. + * This URL's path should end with `/loki/api/v1/push`. + * + * @param url HTTP URL used to connect to Grafana Loki server. + * This URL's path should end with `/loki/api/v1/push`. + * @return This reference. + */ public LogSenderSettings setUrl(final String url) { this.url = url; return this; } + /** + * Getter of HTTP Basic Authentication user. + * + * @return HTTP Basic Authentication user. + */ public String getUser() { return user; } + /** + * Setter of HTTP Basic Authentication user. + * + * @param user HTTP Basic Authentication user. + * @return This reference. + */ public LogSenderSettings setUser(final String user) { this.user = user; return this; } + /** + * Getter of HTTP Basic Authentication password. + * + * @return HTTP Basic Authentication password. + */ public String getPassword() { return password; } + /** + * Setter of HTTP Basic Authentication password. + * + * @param password HTTP Basic Authentication password. + * @return This reference. + */ public LogSenderSettings setPassword(final String password) { this.password = password; return this; } + /** + * Getter of HTTP Content-Type header value. + * + * @return HTTP Content-Type header value. + */ public String getContentType() { return contentType; } + /** + * Setter of HTTP Content-Type header value. + * + * @param contentType HTTP Content-Type header value. + * @return This reference. + */ public LogSenderSettings setContentType(final String contentType) { this.contentType = contentType; return this; } + + /** + * Setter of timeout when connecting to the HTTP server. + * + * @param connectTimeout Time in milliseconds. + * @return This reference. + */ + @SuppressWarnings("unused") + public LogSenderSettings setConnectTimeout(final int connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + /** + * HTTP connecting timeout getter. + * + * @return HTTP connecting timeout in milliseconds. + */ + public int getConnectTimeout() { + return connectTimeout; + } } diff --git a/src/main/java/pl/mjaron/tinyloki/TinyLoki.java b/src/main/java/pl/mjaron/tinyloki/TinyLoki.java index dd1c415..bbc02db 100644 --- a/src/main/java/pl/mjaron/tinyloki/TinyLoki.java +++ b/src/main/java/pl/mjaron/tinyloki/TinyLoki.java @@ -1,11 +1,38 @@ package pl.mjaron.tinyloki; +import java.util.Map; + /** - * Factory method for common objects. + * Factory methods for common objects. */ @SuppressWarnings("unused") public class TinyLoki { + /** + * Creates a basic configuration of LogController. + * + * @param url URL to Loki HTTP API endpoint, usually ending with `/loki/api/v1/push`. + * @param user Basic authentication user. If null, BA header will not be sent. + * @param pass Basic authentication password. If null, BA header will not be sent. + * @return New {@link pl.mjaron.tinyloki.LogController LogController} object. + */ + public static LogController createAndStart(final String url, final String user, final String pass) { + return createAndStart(url, user, pass, LogSenderSettings.DEFAULT_CONNECT_TIMEOUT); + } + + /** + * Creates a basic configuration of LogController. + * + * @param url URL to Loki HTTP API endpoint, usually ending with `/loki/api/v1/push`. + * @param user Basic authentication user. If null, BA header will not be sent. + * @param pass Basic authentication password. If null, BA header will not be sent. + * @param connectTimeout HTTP log server connection timeout in milliseconds. + * @return New {@link pl.mjaron.tinyloki.LogController LogController} object. + */ + public static LogController createAndStart(final String url, final String user, final String pass, final int connectTimeout) { + return createAndStart(url, user, pass, connectTimeout, new JsonLogCollector(), new ErrorLogMonitor()); + } + /** * Creates a configuration of LogController. * @@ -17,9 +44,25 @@ public class TinyLoki { * @return New {@link pl.mjaron.tinyloki.LogController LogController} object. */ public static LogController createAndStart(final String url, final String user, final String pass, final ILogCollector logCollector, ILogMonitor logMonitor) { + return createAndStart(url, user, pass, LogSenderSettings.DEFAULT_CONNECT_TIMEOUT, logCollector, logMonitor); + } + + /** + * Creates a configuration of LogController. + * + * @param url URL to Loki HTTP API endpoint, usually ending with `/loki/api/v1/push`. + * @param user Basic authentication user. If null, BA header will not be sent. + * @param pass Basic authentication password. If null, BA header will not be sent. + * @param connectTimeout HTTP log server connection timeout in milliseconds. + * @param logCollector {@link ILogCollector} instance. + * @param logMonitor {@link ILogMonitor } instance. + * @return New {@link pl.mjaron.tinyloki.LogController LogController} object. + */ + public static LogController createAndStart(final String url, final String user, final String pass, final int connectTimeout, final ILogCollector logCollector, ILogMonitor logMonitor) { LogSenderSettings logSenderSettings = LogSenderSettings.create().setUrl(url); logSenderSettings.setUser(user); logSenderSettings.setPassword(pass); + logSenderSettings.setConnectTimeout(connectTimeout); return new LogController( logCollector, @@ -27,27 +70,25 @@ public static LogController createAndStart(final String url, final String user, logMonitor).start(); } - /** - * Creates a basic configuration of LogController. - * - * @param url URL to Loki HTTP API endpoint, usually ending with `/loki/api/v1/push`. - * @param user Basic authentication user. If null, BA header will not be sent. - * @param pass Basic authentication password. If null, BA header will not be sent. - * @return New {@link pl.mjaron.tinyloki.LogController LogController} object. - */ - public static LogController createAndStart(final String url, final String user, final String pass) { - return createAndStart(url, user, pass, new JsonLogCollector(), new ErrorLogMonitor()); - } - /** * Initialize a {@link pl.mjaron.tinyloki.Labels Labels} with predefined first label name-value. Use Labels.l() to * append next values. * * @param labelName Label name. * @param labelValue Label value. - * @return New Labels instance. + * @return New Labels instance initialized with single label. */ public static Labels l(final String labelName, final String labelValue) { return new Labels().l(labelName, labelValue); } -} + + /** + * Create labels from mapping. + * + * @param map Map containing label names and label values. + * @return New Labels object initialized with labels stored in map. + */ + public static Labels l(final Map map) { + return new Labels().l(map); + } +} \ No newline at end of file diff --git a/src/main/java/pl/mjaron/tinyloki/Utils.java b/src/main/java/pl/mjaron/tinyloki/Utils.java index 76ed74c..a51bd0c 100644 --- a/src/main/java/pl/mjaron/tinyloki/Utils.java +++ b/src/main/java/pl/mjaron/tinyloki/Utils.java @@ -1,9 +1,15 @@ package pl.mjaron.tinyloki; +/** + * Contains universal common methods. + */ public class Utils { /** * Source: https://stackoverflow.com/a/69338077/6835932 + * + * @param b Reference to exiting {@link StringBuilder} where given text will be appended. + * @param text {@link CharSequence} which will be escaped and appended to {@link StringBuilder} b. */ public static void escapeJsonString(final StringBuilder b, final CharSequence text) { for (int i = 0, length = text.length(); i < length; i++) { @@ -27,6 +33,13 @@ public static void escapeJsonString(final StringBuilder b, final CharSequence te } } + /** + * Sleeps given amount of time. Allows calling without try-catch block. + * + * @param milliseconds Milliseconds count. + * @throws RuntimeException When any thread has interrupted this thread. + */ + @SuppressWarnings("unused") public static void sleep(final long milliseconds) { try { Thread.sleep(milliseconds); diff --git a/src/main/java/pl/mjaron/tinyloki/VerboseLogMonitor.java b/src/main/java/pl/mjaron/tinyloki/VerboseLogMonitor.java index 5c4db3d..7207bbd 100644 --- a/src/main/java/pl/mjaron/tinyloki/VerboseLogMonitor.java +++ b/src/main/java/pl/mjaron/tinyloki/VerboseLogMonitor.java @@ -2,6 +2,9 @@ import java.nio.charset.StandardCharsets; +/** + * This implementation is logging all communication between library and HTTP Loki server. + */ public class VerboseLogMonitor extends ErrorLogMonitor { @Override public void send(final byte[] message) { diff --git a/src/main/java/pl/mjaron/tinyloki/third_party/Base64Coder.java b/src/main/java/pl/mjaron/tinyloki/third_party/Base64Coder.java index c61f92c..dd9db6d 100644 --- a/src/main/java/pl/mjaron/tinyloki/third_party/Base64Coder.java +++ b/src/main/java/pl/mjaron/tinyloki/third_party/Base64Coder.java @@ -29,8 +29,7 @@ *

* This class is used to encode and decode data in Base64 format as described in RFC 1521. * - * @author - * Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland, www.source-code.biz + * @author Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland, www.source-code.biz */ public class Base64Coder { @@ -39,90 +38,107 @@ public class Base64Coder { // Mapping table from 6-bit nibbles to Base64 characters. private static final char[] map1 = new char[64]; + static { - int i=0; - for (char c='A'; c<='Z'; c++) map1[i++] = c; - for (char c='a'; c<='z'; c++) map1[i++] = c; - for (char c='0'; c<='9'; c++) map1[i++] = c; - map1[i++] = '+'; map1[i++] = '/'; } + int i = 0; + for (char c = 'A'; c <= 'Z'; c++) map1[i++] = c; + for (char c = 'a'; c <= 'z'; c++) map1[i++] = c; + for (char c = '0'; c <= '9'; c++) map1[i++] = c; + map1[i++] = '+'; + map1[i++] = '/'; + } // Mapping table from Base64 characters to 6-bit nibbles. private static final byte[] map2 = new byte[128]; + static { - for (int i=0; isun.misc.BASE64Encoder.encodeBuffer(byte[]). - * @param in An array containing the data bytes to be encoded. - * @return A String containing the Base64 encoded data, broken into lines. + * + * @param in An array containing the data bytes to be encoded. + * @return A String containing the Base64 encoded data, broken into lines. */ - public static String encodeLines (byte[] in) { - return encodeLines(in, 0, in.length, 76, systemLineSeparator); } + public static String encodeLines(byte[] in) { + return encodeLines(in, 0, in.length, 76, systemLineSeparator); + } /** * Encodes a byte array into Base 64 format and breaks the output into lines. + * * @param in An array containing the data bytes to be encoded. * @param iOff Offset of the first byte in in to be processed. * @param iLen Number of bytes to be processed in in, starting at iOff. * @param lineLen Line length for the output data. Should be a multiple of 4. * @param lineSeparator The line separator to be used to separate the output lines. - * @return A String containing the Base64 encoded data, broken into lines. + * @return A String containing the Base64 encoded data, broken into lines. */ - public static String encodeLines (byte[] in, int iOff, int iLen, int lineLen, String lineSeparator) { - int blockLen = (lineLen*3) / 4; + public static String encodeLines(byte[] in, int iOff, int iLen, int lineLen, String lineSeparator) { + int blockLen = (lineLen * 3) / 4; if (blockLen <= 0) throw new IllegalArgumentException(); - int lines = (iLen+blockLen-1) / blockLen; - int bufLen = ((iLen+2)/3)*4 + lines*lineSeparator.length(); + int lines = (iLen + blockLen - 1) / blockLen; + int bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length(); StringBuilder buf = new StringBuilder(bufLen); int ip = 0; while (ip < iLen) { - int l = Math.min(iLen-ip, blockLen); - buf.append(encode(in, iOff+ip, l)); + int l = Math.min(iLen - ip, blockLen); + buf.append(encode(in, iOff + ip, l)); buf.append(lineSeparator); - ip += l; } - return buf.toString(); } + ip += l; + } + return buf.toString(); + } /** * Encodes a byte array into Base64 format. * No blanks or line breaks are inserted in the output. - * @param in An array containing the data bytes to be encoded. - * @return A character array containing the Base64 encoded data. + * + * @param in An array containing the data bytes to be encoded. + * @return A character array containing the Base64 encoded data. */ - public static char[] encode (byte[] in) { - return encode(in, 0, in.length); } + public static char[] encode(byte[] in) { + return encode(in, 0, in.length); + } /** * Encodes a byte array into Base64 format. * No blanks or line breaks are inserted in the output. - * @param in An array containing the data bytes to be encoded. - * @param iLen Number of bytes to process in in. - * @return A character array containing the Base64 encoded data. + * + * @param in An array containing the data bytes to be encoded. + * @param iLen Number of bytes to process in in. + * @return A character array containing the Base64 encoded data. */ - public static char[] encode (byte[] in, int iLen) { - return encode(in, 0, iLen); } + public static char[] encode(byte[] in, int iLen) { + return encode(in, 0, iLen); + } /** * Encodes a byte array into Base64 format. * No blanks or line breaks are inserted in the output. - * @param in An array containing the data bytes to be encoded. - * @param iOff Offset of the first byte in in to be processed. - * @param iLen Number of bytes to process in in, starting at iOff. - * @return A character array containing the Base64 encoded data. + * + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in in to be processed. + * @param iLen Number of bytes to process in in, starting at iOff. + * @return A character array containing the Base64 encoded data. */ - public static char[] encode (byte[] in, int iOff, int iLen) { - int oDataLen = (iLen*4+2)/3; // output length without padding - int oLen = ((iLen+2)/3)*4; // output length including padding + public static char[] encode(byte[] in, int iOff, int iLen) { + int oDataLen = (iLen * 4 + 2) / 3; // output length without padding + int oLen = ((iLen + 2) / 3) * 4; // output length including padding char[] out = new char[oLen]; int ip = iOff; int iEnd = iOff + iLen; @@ -132,75 +148,90 @@ public static char[] encode (byte[] in, int iOff, int iLen) { int i1 = ip < iEnd ? in[ip++] & 0xff : 0; int i2 = ip < iEnd ? in[ip++] & 0xff : 0; int o0 = i0 >>> 2; - int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); int o3 = i2 & 0x3F; out[op++] = map1[o0]; out[op++] = map1[o1]; - out[op] = op < oDataLen ? map1[o2] : '='; op++; - out[op] = op < oDataLen ? map1[o3] : '='; op++; } - return out; } + out[op] = op < oDataLen ? map1[o2] : '='; + op++; + out[op] = op < oDataLen ? map1[o3] : '='; + op++; + } + return out; + } /** * Decodes a string from Base64 format. * No blanks or line breaks are allowed within the Base64 encoded input data. - * @param s A Base64 String to be decoded. - * @return A String containing the decoded data. - * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + * + * @param s A Base64 String to be decoded. + * @return A String containing the decoded data. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ - public static String decodeString (String s) { - return new String(decode(s)); } + public static String decodeString(String s) { + return new String(decode(s)); + } /** * Decodes a byte array from Base64 format and ignores line separators, tabs and blanks. * CR, LF, Tab and Space characters are ignored in the input data. * This method is compatible with sun.misc.BASE64Decoder.decodeBuffer(String). - * @param s A Base64 String to be decoded. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + * + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ - public static byte[] decodeLines (String s) { + public static byte[] decodeLines(String s) { char[] buf = new char[s.length()]; int p = 0; for (int ip = 0; ip < s.length(); ip++) { char c = s.charAt(ip); if (c != ' ' && c != '\r' && c != '\n' && c != '\t') - buf[p++] = c; } - return decode(buf, 0, p); } + buf[p++] = c; + } + return decode(buf, 0, p); + } /** * Decodes a byte array from Base64 format. * No blanks or line breaks are allowed within the Base64 encoded input data. - * @param s A Base64 String to be decoded. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + * + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ - public static byte[] decode (String s) { - return decode(s.toCharArray()); } + public static byte[] decode(String s) { + return decode(s.toCharArray()); + } /** * Decodes a byte array from Base64 format. * No blanks or line breaks are allowed within the Base64 encoded input data. - * @param in A character array containing the Base64 encoded data. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + * + * @param in A character array containing the Base64 encoded data. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ - public static byte[] decode (char[] in) { - return decode(in, 0, in.length); } + public static byte[] decode(char[] in) { + return decode(in, 0, in.length); + } /** * Decodes a byte array from Base64 format. * No blanks or line breaks are allowed within the Base64 encoded input data. - * @param in A character array containing the Base64 encoded data. - * @param iOff Offset of the first character in in to be processed. - * @param iLen Number of characters to process in in, starting at iOff. - * @return An array containing the decoded data bytes. - * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + * + * @param in A character array containing the Base64 encoded data. + * @param iOff Offset of the first character in in to be processed. + * @param iLen Number of characters to process in in, starting at iOff. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. */ - public static byte[] decode (char[] in, int iOff, int iLen) { - if (iLen%4 != 0) throw new IllegalArgumentException("Length of Base64 encoded input string is not a multiple of 4."); - while (iLen > 0 && in[iOff+iLen-1] == '=') iLen--; - int oLen = (iLen*3) / 4; + public static byte[] decode(char[] in, int iOff, int iLen) { + if (iLen % 4 != 0) + throw new IllegalArgumentException("Length of Base64 encoded input string is not a multiple of 4."); + while (iLen > 0 && in[iOff + iLen - 1] == '=') iLen--; + int oLen = (iLen * 3) / 4; byte[] out = new byte[oLen]; int ip = iOff; int iEnd = iOff + iLen; @@ -218,15 +249,18 @@ public static byte[] decode (char[] in, int iOff, int iLen) { int b3 = map2[i3]; if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) throw new IllegalArgumentException("Illegal character in Base64 encoded data."); - int o0 = ( b0 <<2) | (b1>>>4); - int o1 = ((b1 & 0xf)<<4) | (b2>>>2); - int o2 = ((b2 & 3)<<6) | b3; - out[op++] = (byte)o0; - if (op>> 4); + int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); + int o2 = ((b2 & 3) << 6) | b3; + out[op++] = (byte) o0; + if (op < oLen) out[op++] = (byte) o1; + if (op < oLen) out[op++] = (byte) o2; + } + return out; + } // Dummy constructor. - private Base64Coder() {} + private Base64Coder() { + } } // end class Base64Coder diff --git a/src/test/java/pl/mjaron/tinyloki/LogCollectorTest.java b/src/test/java/pl/mjaron/tinyloki/LogCollectorTest.java index d753417..8755fa0 100644 --- a/src/test/java/pl/mjaron/tinyloki/LogCollectorTest.java +++ b/src/test/java/pl/mjaron/tinyloki/LogCollectorTest.java @@ -34,11 +34,11 @@ void basicJson() { @Test @Disabled - void logController() { - LogController logController = TinyLoki.createAndStart("http://localhost/loki/api/v1/push", "user", "pass"); + void tinyLokiTest() { + LogController logController = TinyLoki.createAndStart("http://localhost/loki/api/v1/push", "user", "pass", 5000); ILogStream stream = logController.createStream(TinyLoki.l(Labels.LEVEL, Labels.INFO).l("host", "ZEUS")); stream.log("Hello world."); // ... new streams and other logs here. - logController.softStop().waitForStop(); + logController.softStop().hardStop(); } }