Skip to content

Commit d5b93fc

Browse files
committed
Java util log handler implementation.
1 parent 7f713d2 commit d5b93fc

5 files changed

Lines changed: 223 additions & 6 deletions

File tree

README.md

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ Unit tests
1919

2020
Tiny [Grafana Loki](https://grafana.com/oss/loki/) client (log sender) written in pure Java 1.8 without any external
2121
dependencies. One of Grafana Loki third-party clients mentioned
22-
in [documentation](https://grafana.com/docs/loki/v3.4.x/send-data/).
22+
in [documentation](https://grafana.com/docs/loki/v3.4.x/send-data/). It is customizable and easy to integrate
23+
but is not optimized for performance.
24+
See other reliable clients mentioned in documentation.
2325

2426
* Implements JSON variant of [Loki API](https://grafana.com/docs/loki/latest/api/#post-lokiapiv1push)
2527
* Works with **Android** and **Java SE**
2628
* Thread safe
29+
* May added to [java.util.logging](https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html)
30+
as a log [Handler](https://docs.oracle.com/javase/8/docs/api/java/util/logging/Handler.html)
31+
(and then may be called from [SLF4J](https://www.slf4j.org/)
32+
using [slf4j-jdk14 provider](https://www.slf4j.org/manual.html)).
2733

2834
## Examples
2935

@@ -139,6 +145,35 @@ public class Sample {
139145
}
140146
```
141147

148+
### SLF4J with java.util.logging example
149+
150+
```groovy
151+
dependencies {
152+
implementation("org.slf4j:slf4j-jdk14:2.0.17")
153+
implementation("org.slf4j:slf4j-api:2.0.17")
154+
implementation("io.github.mjfryc:mjaron-tinyloki-java:1.1.6")
155+
}
156+
```
157+
158+
```java
159+
import pl.mjaron.tinyloki.*;
160+
import org.slf4j.Logger;
161+
import org.slf4j.LoggerFactory;
162+
163+
public class Sample {
164+
public static void main(String[] args) {
165+
166+
TinyLokiJulHandler.install(TinyLoki
167+
.withUrl("http://localhost:3100")
168+
.withBasicAuth("user", "pass")
169+
.withLabels(Labels.of(Labels.SERVICE_NAME, "integration_test")));
170+
171+
Logger logger = LoggerFactory.getLogger("julIntegrationTest");
172+
logger.info("Hello info.");
173+
}
174+
}
175+
```
176+
142177
## Integration
143178

144179
### Maven Central
@@ -214,7 +249,8 @@ If this size is exceeded, the server may respond with HTTP error `500`, e.g:
214249
rpc error: code = ResourceExhausted desc = grpc: received message larger than max (6331143 vs. 4194304)
215250
```
216251

217-
By default (`BasicBuffering`) The TinyLoki tries to fill the data buffers and if the next log would exceed the size limit,
252+
By default (`BasicBuffering`) The TinyLoki tries to fill the data buffers and if the next log would exceed the size
253+
limit,
218254
new buffer is allocated instead. Custom buffer size may be configured with:
219255

220256
```java
@@ -299,15 +335,14 @@ classDiagram
299335
300336
VerboseLogMonitor --|> ILogMonitor: implements
301337
ErrorLogMonitor --|> ILogMonitor: implements
302-
303338
ILogStream <.. ILogCollector: create
304339
ILogCollector --* TinyLoki
305-
Labels <.. ILogCollector : use
340+
Labels <.. ILogCollector: use
306341
ILogSender --* TinyLoki
307342
GzipLogEncoder --|> ILogEncoder: implements
308343
ILogEncoder --* TinyLoki
309344
ILogMonitor --* TinyLoki
310-
JsonLogStream --|> ILogStream: implements
345+
JsonLogStream --|> ILogStream: implements
311346
JsonLogCollector --|> ILogCollector: implements
312347
JsonLogStream <.. JsonLogCollector: create
313348
HttpLogSender --|> ILogSender: implements

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group 'io.github.mjfryc'
9-
version '1.1.5'
9+
version '1.1.6'
1010

1111
repositories {
1212
mavenCentral()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package pl.mjaron.tinyloki;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.Objects;
6+
import java.util.logging.Level;
7+
import java.util.logging.LogRecord;
8+
9+
/**
10+
* The log appender which writes logs from the {@link java.util.logging.Logger java.util.logging} library.
11+
* It may be initialized with one of the {@link #install(Settings)} static methods.
12+
*
13+
* @since 1.1.6
14+
*/
15+
public class TinyLokiJulHandler extends java.util.logging.Handler {
16+
17+
/**
18+
* Attaches the logger to root logger.
19+
*
20+
* @param settings The {@link TinyLoki} library {@link Settings}.
21+
* @return Created {@link TinyLoki} instance.
22+
*/
23+
public static TinyLoki install(Settings settings) {
24+
java.util.logging.Logger rootLogger = java.util.logging.LogManager.getLogManager().getLogger("");
25+
TinyLokiJulHandler handler = new TinyLokiJulHandler(settings);
26+
rootLogger.addHandler(handler);
27+
return handler.getLoki();
28+
}
29+
30+
public static void install(String url) {
31+
install(TinyLoki.withUrl(url));
32+
}
33+
34+
public static void install(String url, String user, String pass) {
35+
install(TinyLoki.withUrl(url).withBasicAuth(user, pass));
36+
}
37+
38+
private static class StreamKey {
39+
final String tag;
40+
final Level level;
41+
final int hash;
42+
43+
private StreamKey(String tag, Level level) {
44+
this.tag = tag;
45+
this.level = level;
46+
this.hash = Objects.hash(tag, level);
47+
}
48+
49+
@Override
50+
public boolean equals(Object o) {
51+
if (this == o) return true;
52+
if (o == null || getClass() != o.getClass()) return false;
53+
StreamKey that = (StreamKey) o;
54+
return tag.equals(that.tag) && level == that.level;
55+
}
56+
57+
@Override
58+
public int hashCode() {
59+
return this.hash;
60+
}
61+
}
62+
63+
private static String toTinyLokiLogLevel(final Level level) {
64+
final int value = level.intValue();
65+
if (value >= 1000) { // SEVERE
66+
return Labels.FATAL;
67+
} else if (value >= 900) { // WARNING
68+
return Labels.WARN;
69+
} else if (value >= 800) { // INFO
70+
return Labels.INFO;
71+
} else if (value >= 700) { // CONFIG
72+
return Labels.DEBUG;
73+
} else {
74+
return Labels.VERBOSE;
75+
}
76+
}
77+
78+
private final TinyLoki loki;
79+
private Map<StreamKey, ILogStream> streams = new HashMap<>();
80+
81+
public TinyLokiJulHandler(TinyLoki loki) {
82+
this.loki = loki;
83+
}
84+
85+
public TinyLokiJulHandler(Settings settings) {
86+
loki = TinyLoki.open(settings);
87+
}
88+
89+
public TinyLoki getLoki() {
90+
return loki;
91+
}
92+
93+
@Override
94+
synchronized public void publish(LogRecord logRecord) {
95+
StreamKey key = new StreamKey(logRecord.getLoggerName(), logRecord.getLevel());
96+
ILogStream stream = streams.get(key);
97+
if (stream == null) {
98+
Labels streamLabels = Labels.of().l("tag", logRecord.getLoggerName()).l(Labels.LEVEL, toTinyLokiLogLevel(logRecord.getLevel()));
99+
stream = loki.openStream(streamLabels);
100+
streams.put(key, stream);
101+
}
102+
stream.log(logRecord.getMessage());
103+
}
104+
105+
@Override
106+
public void flush() {
107+
try {
108+
loki.sync();
109+
} catch (InterruptedException ignored) {
110+
Thread.currentThread().interrupt();
111+
}
112+
}
113+
114+
@Override
115+
public void close() throws SecurityException {
116+
try {
117+
loki.closeSync();
118+
} catch (InterruptedException ignored) {
119+
Thread.currentThread().interrupt();
120+
}
121+
}
122+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package pl.mjaron.tinyloki;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
5+
6+
import java.util.logging.Logger;
7+
8+
/**
9+
* To enable this test, modify the run configuration by adding environment variable:
10+
* <pre>{@code
11+
* TINYLOKI_INTEGRATION=1
12+
* }</pre>
13+
* <p>
14+
* Integration test server must be enabled to receive logs, see <code>integration-test-server</code> project directory.
15+
*/
16+
@EnabledIfEnvironmentVariable(named = "TINYLOKI_INTEGRATION", matches = "1")
17+
public class JulIntegrationTest {
18+
// @formatter:off
19+
20+
@Test
21+
void julIntegrationTest()throws InterruptedException {
22+
// Optionally the TinyLoki instance may be gained.
23+
// TinyLoki loki =
24+
25+
TinyLokiJulHandler.install(TinyLoki.withUrl("http://localhost:3100")
26+
.withBasicAuth("user", "pass")
27+
.withLabels(Labels.of(Labels.SERVICE_NAME, "integration_test")));
28+
Logger logger = Logger.getLogger("julIntegrationTest");
29+
logger.info("Hello info.");
30+
31+
// Optionally, additional logs synchronization may be performed.
32+
// loki.sync();
33+
}
34+
35+
// @formatter:on
36+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package pl.mjaron.tinyloki;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
class TinyLokiJulHandlerTest {
8+
9+
@Test
10+
void basicTest() {
11+
MemoryLogSender memoryLogSender = new MemoryLogSender();
12+
13+
// @formatter:off
14+
TinyLokiJulHandler.install(TinyLoki.withUrl("dummy_url")
15+
.withLogSender(memoryLogSender)
16+
.withExecutor(new SyncExecutor())
17+
.withVerboseLogMonitor(true));
18+
// @formatter:on
19+
java.util.logging.Logger logger = java.util.logging.Logger.getLogger("julBasicTest");
20+
logger.info("Hello world.");
21+
String data = memoryLogSender.getAsString();
22+
assertTrue(data.contains("Hello world."));
23+
}
24+
}

0 commit comments

Comments
 (0)