Skip to content

Commit 89b272e

Browse files
committed
Issue #30: Solution for duplicated dropped logs.
* ITimestampProvider introduced. * Refactoring. * v1.1.3
1 parent 6ba3c15 commit 89b272e

20 files changed

Lines changed: 329 additions & 129 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package pl.mjaron.tinyloki;
2+
3+
/**
4+
* Basic implementation.
5+
* <p>
6+
* Provides current timestamp provided by {@link System#currentTimeMillis()}
7+
* and converted to nanoseconds by multiplying by <code>1_000_000</code>.
8+
*
9+
* @since 1.1.3
10+
*/
11+
public class CurrentTimestampProvider implements ITimestampProvider {
12+
13+
@Override
14+
public long next(String message) {
15+
return Utils.Nanoseconds.currentTime();
16+
}
17+
18+
/**
19+
* Factory of {@link CurrentTimestampProvider}.
20+
*/
21+
public static class Factory implements ITimestampProviderFactory {
22+
23+
@Override
24+
public ITimestampProvider create() {
25+
return new CurrentTimestampProvider();
26+
}
27+
}
28+
29+
public static ITimestampProviderFactory factory() {
30+
return new Factory();
31+
}
32+
}

src/main/java/pl/mjaron/tinyloki/IExecutor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*
77
* @since 1.0.0
88
*/
9-
public interface IExecutor {
9+
public interface IExecutor extends ILogListener {
1010

1111
/**
1212
* Performs the configuration before using the object. Called internally by the {@link TinyLoki} constructor.

src/main/java/pl/mjaron/tinyloki/ILogCollector.java

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,15 @@
66
public interface ILogCollector {
77

88
/**
9-
* Called before using the object.
10-
*
11-
* @param logListener The object used as callback when new log occurs.
12-
* @since 1.0.0
13-
*/
14-
void configureLogListener(ILogListener logListener);
15-
16-
/**
17-
* Called before using the object.
18-
*
19-
* @param bufferingManager The object which determines sent data buffering policy.
20-
* @since 1.0.0
21-
*/
22-
void configureBufferingManager(IBuffering bufferingManager);
23-
24-
/**
25-
* If label settings are not <code>null</code>, the structured metadata is supported, else structured metadata should not be collected.
9+
* Called internally by {@link TinyLoki} to configure the object before using it.
2610
*
11+
* @param logListener The object used as callback when new log occurs.
12+
* @param bufferingManager The object which determines sent data buffering policy.
2713
* @param structuredMetadataLabelSettings Settings for structured metadata validation.
28-
* @since 1.1.0
29-
*/
30-
void configureStructuredMetadata(LabelSettings structuredMetadataLabelSettings);
31-
32-
/**
33-
* Configures all values in single command.
34-
*
35-
* @param logListener The object used as callback when new log occurs.
36-
* @param bufferingManager The object which determines sent data buffering policy.
37-
* @since 1.0.0
14+
* @param timestampProviderFactory The factory creating {@link ITimestampProvider} objects.
15+
* @since 1.1.3
3816
*/
39-
default void configure(ILogListener logListener, IBuffering bufferingManager) {
40-
configureLogListener(logListener);
41-
configureBufferingManager(bufferingManager);
42-
}
17+
void configure(ILogListener logListener, IBuffering bufferingManager, LabelSettings structuredMetadataLabelSettings, ITimestampProviderFactory timestampProviderFactory);
4318

4419
/**
4520
* Creates a new stream.

src/main/java/pl/mjaron/tinyloki/ILogStream.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,32 @@
1010
*/
1111
public interface ILogStream {
1212

13+
/**
14+
* Specifies that the stream should determine the current timestamp itself.
15+
*
16+
* @since 1.1.3
17+
*/
18+
long TIMESTAMP_NONE = -1;
19+
1320
/**
1421
* Thread-safe method used to write log messages to a stream.
1522
*
16-
* @param timestampMs Usually System.currentTimeMillis().
23+
* @param timestampNs Log timestamp in <b>nanoseconds</b>.
1724
* @param line Log content.
1825
* @param structuredMetadata The optional (nullable) {@link Labels} containing <a href="https://grafana.com/docs/loki/v3.4.x/get-started/labels/structured-metadata/">structured metadata</a> values.
19-
* @since 1.1.0
26+
* @since 1.1.3
2027
*/
21-
void log(final long timestampMs, final String line, final Labels structuredMetadata);
28+
void log(final long timestampNs, final String line, final Labels structuredMetadata);
2229

2330
/**
2431
* Thread-safe log line with custom time.
2532
*
26-
* @param timestampMs Log timestamp in milliseconds.
33+
* @param timestampNs Log timestamp in <b>nanoseconds</b>.
2734
* @param line Log content.
28-
* @since 1.1.0
35+
* @since 1.1.3
2936
*/
30-
default void log(final long timestampMs, final String line) {
31-
log(timestampMs, line, null);
37+
default void log(final long timestampNs, final String line) {
38+
log(timestampNs, line, null);
3239
}
3340

3441
/**
@@ -37,7 +44,7 @@ default void log(final long timestampMs, final String line) {
3744
* @param line Log content.
3845
*/
3946
default void log(final String line) {
40-
log(System.currentTimeMillis(), line, null);
47+
log(TIMESTAMP_NONE, line, null);
4148
}
4249

4350
/**
@@ -48,7 +55,7 @@ default void log(final String line) {
4855
* @since 1.1.0
4956
*/
5057
default void log(final String line, final Labels structuredMetadata) {
51-
log(System.currentTimeMillis(), line, structuredMetadata);
58+
log(TIMESTAMP_NONE, line, structuredMetadata);
5259
}
5360

5461
/**
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package pl.mjaron.tinyloki;
2+
3+
/**
4+
* Provides the log occurrence timestamp in <b>nanoseconds</b>.
5+
* <p>
6+
* <b>Motivation</b><br>
7+
* Grafana Loki treats two identical log messages with the same timestamp as a single log.
8+
* The second log is ignored, even if the structured metadata is different.
9+
* <p>
10+
* User may want to differentiate the timestamps.
11+
* This is the reason why the creation of timestamp values is configurable.
12+
*
13+
* @see ITimestampProviderFactory
14+
* @see CurrentTimestampProvider
15+
* @see IncrementingTimestampProvider
16+
* @since 1.1.3
17+
*/
18+
public interface ITimestampProvider {
19+
20+
/**
21+
* Provides log timestamp value in nanoseconds.
22+
* The point is to optionally avoid two identical log messages
23+
* having the same timestamp.
24+
* <p>
25+
* <b>Thread safety</b><br>
26+
* This method may not be thread safe and should be synchronized internally
27+
* by {@link ILogStream} or {@link ILogCollector} implementation.
28+
*
29+
* @param message Original log message. It may be analyzed by any {@link ITimestampProvider} implementation.
30+
* @return The calculated timestamp.
31+
* @since 1.1.3
32+
*/
33+
long next(String message);
34+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package pl.mjaron.tinyloki;
2+
3+
/**
4+
* Creates new instance of {@link ITimestampProvider} implementation.
5+
*
6+
* @since 1.1.3
7+
*/
8+
public interface ITimestampProviderFactory {
9+
10+
/**
11+
* Creates new instance of {@link ITimestampProvider} implementation.
12+
*
13+
* @return New instance of {@link ITimestampProvider} implementation.
14+
* @since 1.1.3
15+
*/
16+
ITimestampProvider create();
17+
18+
/**
19+
* Provides given <code>factory</code> or the {@link #getDefault() default} implementation if parameter is <code>null</code>.
20+
*
21+
* @param factory The original factory or <code>null</code> value.
22+
* @return If not a <code>null</code>, the <code>factory</code> parameter, otherwise the {@link #getDefault() default} implementation.
23+
* @since 1.1.3
24+
*/
25+
static ITimestampProviderFactory orDefault(ITimestampProviderFactory factory) {
26+
if (factory == null) {
27+
return getDefault();
28+
}
29+
return factory;
30+
}
31+
32+
/**
33+
* @return New factory of {@link CurrentTimestampProvider}.
34+
*/
35+
static ITimestampProviderFactory current() {
36+
return CurrentTimestampProvider.factory();
37+
}
38+
39+
/**
40+
* @return New factory of {@link IncrementingTimestampProvider}.
41+
*/
42+
static ITimestampProviderFactory incrementing() {
43+
return IncrementingTimestampProvider.factory();
44+
}
45+
46+
/**
47+
* @return {@link CurrentTimestampProvider}.
48+
*/
49+
static ITimestampProviderFactory getDefault() {
50+
return current();
51+
}
52+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package pl.mjaron.tinyloki;
2+
3+
/**
4+
* Provides uniqueness of logged time for first 999999 logs to the same
5+
* millisecond with little impact on logging performance.
6+
*
7+
* @since 1.1.3
8+
*/
9+
public class IncrementingTimestampProvider implements ITimestampProvider {
10+
11+
private long lastCurrentTimeMillis = 0;
12+
private int nanoseconds = 0;
13+
14+
@Override
15+
public long next(String message) {
16+
final long current = System.currentTimeMillis();
17+
if (current == lastCurrentTimeMillis) {
18+
if (nanoseconds < 999_999) {
19+
++nanoseconds;
20+
}
21+
return lastCurrentTimeMillis * 1_000_000 + nanoseconds;
22+
} else {
23+
lastCurrentTimeMillis = current;
24+
nanoseconds = 0;
25+
return lastCurrentTimeMillis * 1_000_000;
26+
}
27+
}
28+
29+
/**
30+
* Factory of {@link IncrementingTimestampProvider}.
31+
*/
32+
public static class Factory implements ITimestampProviderFactory {
33+
34+
@Override
35+
public ITimestampProvider create() {
36+
return new IncrementingTimestampProvider();
37+
}
38+
}
39+
40+
public static ITimestampProviderFactory factory() {
41+
return new Factory();
42+
}
43+
}

src/main/java/pl/mjaron/tinyloki/JsonLogCollector.java

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,14 @@ public class JsonLogCollector implements ILogCollector {
1717
private ILogListener logListener = null;
1818
private IBuffering bufferingManager = null;
1919
private LabelSettings structuredMetadataLabelSettings = null;
20+
private ITimestampProvider timestampProvider = null;
2021

2122
@Override
22-
public void configureLogListener(final ILogListener logListener) {
23+
public void configure(ILogListener logListener, IBuffering bufferingManager, LabelSettings structuredMetadataLabelSettings, ITimestampProviderFactory timestampProviderFactory) {
2324
this.logListener = logListener;
24-
}
25-
26-
@Override
27-
public void configureBufferingManager(final IBuffering bufferingManager) {
2825
this.bufferingManager = bufferingManager;
29-
}
30-
31-
@Override
32-
public void configureStructuredMetadata(LabelSettings structuredMetadataLabelSettings) {
3326
this.structuredMetadataLabelSettings = structuredMetadataLabelSettings;
27+
this.timestampProvider = ITimestampProviderFactory.orDefault(timestampProviderFactory).create();
3428
}
3529

3630
/**
@@ -126,13 +120,16 @@ public String contentType() {
126120
return CONTENT_TYPE;
127121
}
128122

129-
synchronized void logImplementation(JsonLogStream stream, final long timestampMs, final String line, final Labels structuredMetadata) {
123+
synchronized void logImplementation(JsonLogStream stream, long timestamp, final String line, final Labels structuredMetadata) {
130124
final int logCandidateSize = 25 + line.length();
131125
if (!bufferingManager.beforeLog(logCandidateSize)) {
132126
return;
133127
}
134128

135-
final int acceptedLogSize = stream.logUnsafe(timestampMs, line, structuredMetadata);
129+
if (timestamp == ILogStream.TIMESTAMP_NONE) {
130+
timestamp = timestampProvider.next(line);
131+
}
132+
final int acceptedLogSize = stream.logUnsafe(timestamp, line, structuredMetadata);
136133

137134
bufferingManager.logAccepted(acceptedLogSize);
138135
++logEntriesCount;

src/main/java/pl/mjaron/tinyloki/JsonLogStream.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ public JsonLogStream(final JsonLogCollector collector, final Labels labels) {
4242
initialSequenceWithHeaders = b.toString();
4343
}
4444

45-
int logUnsafe(final long timestampMs, final String line, final Labels structuredMetadata) {
45+
int logUnsafe(final long timestamp, final String line, final Labels structuredMetadata) {
4646
final int beforeSize = b.length();
4747
if (cachedLogsCount != 0) {
4848
b.append(',');
4949
}
5050
++cachedLogsCount;
5151
b.append("[\"");
52-
b.append(timestampMs);
53-
b.append("000000\",\"");
52+
b.append(timestamp);
53+
b.append("\",\"");
5454
Utils.escapeJsonString(b, line);
5555
b.append('"');
5656

@@ -78,8 +78,8 @@ int logUnsafe(final long timestampMs, final String line, final Labels structured
7878
}
7979

8080
@Override
81-
public void log(final long timestampMs, final String line, final Labels structuredMetadata) {
82-
collector.logImplementation(this, timestampMs, line, structuredMetadata);
81+
public void log(final long timestampNs, final String line, final Labels structuredMetadata) {
82+
collector.logImplementation(this, timestampNs, line, structuredMetadata);
8383
}
8484

8585
@Override

0 commit comments

Comments
 (0)