summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@verizonmedia.com>2021-01-22 18:33:09 +0100
committerBjørn Christian Seime <bjorncs@verizonmedia.com>2021-01-25 19:01:57 +0100
commitb24a4cecfc9418d0613fa3fb90309bfdae723f18 (patch)
tree8c89efe4e6679478e12eb7d496e330e6a90ae447
parent80a329ba15c6b1cde5d12239b5b7c568156bdb65 (diff)
Write connection log entry directly to output stream
-rw-r--r--jdisc_http_service/pom.xml5
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java76
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java13
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java46
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java25
-rw-r--r--jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java117
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/container/logging/ConnectionLogEntryTest.java32
-rw-r--r--jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java36
8 files changed, 210 insertions, 140 deletions
diff --git a/jdisc_http_service/pom.xml b/jdisc_http_service/pom.xml
index 68a0f0636d3..2baba974b03 100644
--- a/jdisc_http_service/pom.xml
+++ b/jdisc_http_service/pom.xml
@@ -151,6 +151,11 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
<plugins>
diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java
index 15a38f6eeae..a6b800a9279 100644
--- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java
+++ b/jdisc_http_service/src/main/java/com/yahoo/container/logging/ConnectionLogEntry.java
@@ -2,13 +2,6 @@
package com.yahoo.container.logging;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.slime.Type;
-import com.yahoo.yolean.Exceptions;
-
-import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
@@ -69,75 +62,6 @@ public class ConnectionLogEntry {
this.sslHandshakeFailureType = builder.sslHandshakeFailureType;
}
- public String toJson() {
- Slime slime = new Slime();
- Cursor cursor = slime.setObject();
- cursor.setString("id", id.toString());
- setTimestamp(cursor, timestamp, "timestamp");
-
- setDouble(cursor, durationSeconds, "duration");
- setString(cursor, peerAddress, "peerAddress");
- setInteger(cursor, peerPort, "peerPort");
- setString(cursor, localAddress, "localAddress");
- setInteger(cursor, localPort, "localPort");
- setString(cursor, remoteAddress, "remoteAddress");
- setInteger(cursor, remotePort, "remotePort");
- setLong(cursor, httpBytesReceived, "httpBytesReceived");
- setLong(cursor, httpBytesSent, "httpBytesSent");
- setLong(cursor, requests, "requests");
- setLong(cursor, responses, "responses");
- setString(cursor, sslProtocol, "ssl", "protocol");
- setString(cursor, sslSessionId, "ssl", "sessionId");
- setString(cursor, sslCipherSuite, "ssl", "cipherSuite");
- setString(cursor, sslPeerSubject, "ssl", "peerSubject");
- setTimestamp(cursor, sslPeerNotBefore, "ssl", "peerNotBefore");
- setTimestamp(cursor, sslPeerNotAfter, "ssl", "peerNotAfter");
- setString(cursor, sslSniServerName, "ssl", "sniServerName");
- setString(cursor, sslHandshakeFailureException, "ssl", "handshake-failure", "exception");
- setString(cursor, sslHandshakeFailureMessage, "ssl", "handshake-failure", "message");
- setString(cursor, sslHandshakeFailureType, "ssl", "handshake-failure", "type");
- return new String(Exceptions.uncheck(() -> SlimeUtils.toJsonBytes(slime)), StandardCharsets.UTF_8);
- }
-
- private void setString(Cursor cursor, String value, String... keys) {
- if(value != null) {
- subCursor(cursor, keys).setString(keys[keys.length - 1], value);
- }
- }
-
- private void setLong(Cursor cursor, Long value, String... keys) {
- if (value != null) {
- subCursor(cursor, keys).setLong(keys[keys.length - 1], value);
- }
- }
-
- private void setInteger(Cursor cursor, Integer value, String... keys) {
- if (value != null) {
- subCursor(cursor, keys).setLong(keys[keys.length - 1], value);
- }
- }
-
- private void setTimestamp(Cursor cursor, Instant timestamp, String... keys) {
- if (timestamp != null) {
- subCursor(cursor, keys).setString(keys[keys.length - 1], timestamp.toString());
- }
- }
-
- private void setDouble(Cursor cursor, Double value, String... keys) {
- if (value != null) {
- subCursor(cursor, keys).setDouble(keys[keys.length - 1], value);
- }
- }
-
- private static Cursor subCursor(Cursor cursor, String... keys) {
- Cursor subCursor = cursor;
- for (int i = 0; i < keys.length - 1; ++i) {
- Cursor field = subCursor.field(keys[i]);
- subCursor = field.type() != Type.NIX ? field : subCursor.setObject(keys[i]);
- }
- return subCursor;
- }
-
public static Builder builder(UUID id, Instant timestamp) {
return new Builder(id, timestamp);
}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java
index 62e53a5a514..968ba74b4f2 100644
--- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java
+++ b/jdisc_http_service/src/main/java/com/yahoo/container/logging/FileConnectionLog.java
@@ -5,23 +5,19 @@ package com.yahoo.container.logging;
import com.google.inject.Inject;
import com.yahoo.component.AbstractComponent;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.logging.Level;
import java.util.logging.Logger;
/**
* @author mortent
*/
-public class FileConnectionLog extends AbstractComponent implements ConnectionLog, LogWriter<ConnectionLogEntry> {
+public class FileConnectionLog extends AbstractComponent implements ConnectionLog {
private static final Logger logger = Logger.getLogger(FileConnectionLog.class.getName());
private final ConnectionLogHandler logHandler;
@Inject
public FileConnectionLog(ConnectionLogConfig config) {
- logHandler = new ConnectionLogHandler(config.cluster(), this);
+ logHandler = new ConnectionLogHandler(config.cluster(), new JsonConnectionLogWriter());
}
@Override
@@ -34,9 +30,4 @@ public class FileConnectionLog extends AbstractComponent implements ConnectionLo
logHandler.shutdown();
}
- @Override
- // TODO serialize directly to outputstream
- public void write(ConnectionLogEntry entry, OutputStream outputStream) throws IOException {
- outputStream.write(entry.toJson().getBytes(StandardCharsets.UTF_8));
- }
} \ No newline at end of file
diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java
new file mode 100644
index 00000000000..ee780ad2a83
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/container/logging/FormatUtil.java
@@ -0,0 +1,46 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.logging;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * @author bjorncs
+ */
+class FormatUtil {
+
+ private FormatUtil() {}
+
+ static void writeSecondsField(JsonGenerator generator, String fieldName, Instant instant) throws IOException {
+ writeSecondsField(generator, fieldName, instant.toEpochMilli());
+ }
+
+ static void writeSecondsField(JsonGenerator generator, String fieldName, Duration duration) throws IOException {
+ writeSecondsField(generator, fieldName, duration.toMillis());
+ }
+
+ static void writeSecondsField(JsonGenerator generator, String fieldName, double seconds) throws IOException {
+ writeSecondsField(generator, fieldName, (long)(seconds * 1000));
+ }
+
+ static void writeSecondsField(JsonGenerator generator, String fieldName, long milliseconds) throws IOException {
+ generator.writeFieldName(fieldName);
+ generator.writeRawValue(toSecondsString(milliseconds));
+ }
+
+ /** @return a string with number of seconds with 3 decimals */
+ static String toSecondsString(long milliseconds) {
+ StringBuilder builder = new StringBuilder().append(milliseconds / 1000L).append('.');
+ long decimals = milliseconds % 1000;
+ if (decimals < 100) {
+ builder.append('0');
+ if (decimals < 10) {
+ builder.append('0');
+ }
+ }
+ return builder.append(decimals).toString();
+ }
+}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java
index c6d177684ac..441e139bc67 100644
--- a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java
+++ b/jdisc_http_service/src/main/java/com/yahoo/container/logging/JSONFormatter.java
@@ -15,6 +15,8 @@ import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
+import static com.yahoo.container.logging.FormatUtil.writeSecondsField;
+
/**
* Formatting of an {@link AccessLogEntry} in the Vespa JSON access log format.
*
@@ -45,8 +47,8 @@ public class JSONFormatter implements LogWriter<RequestLogEntry> {
String peerAddress = entry.peerAddress().get();
generator.writeStringField("ip", peerAddress);
long time = entry.timestamp().get().toEpochMilli();
- writeSeconds(generator, "time", time);
- writeSeconds(generator, "duration", entry.duration().get().toMillis());
+ FormatUtil.writeSecondsField(generator, "time", time);
+ FormatUtil.writeSecondsField(generator, "duration", entry.duration().get());
generator.writeNumberField("responsesize", entry.contentSize().orElse(0));
generator.writeNumberField("code", entry.statusCode().orElse(0));
generator.writeStringField("method", entry.httpMethod().orElse(""));
@@ -185,23 +187,4 @@ public class JSONFormatter implements LogWriter<RequestLogEntry> {
if (rawPath == null) return null;
return rawQuery != null ? rawPath + "?" + rawQuery : rawPath;
}
-
- private static void writeSeconds(JsonGenerator generator, String fieldName, long milliseconds) throws IOException {
- generator.writeFieldName(fieldName);
- generator.writeRawValue(toSecondsString(milliseconds));
- }
-
- /** @return a string with number of seconds with 3 decimals */
- private static String toSecondsString(long milliseconds) {
- StringBuilder builder = new StringBuilder().append(milliseconds / 1000L).append('.');
- long decimals = milliseconds % 1000;
- if (decimals < 100) {
- builder.append('0');
- if (decimals < 10) {
- builder.append('0');
- }
- }
- return builder.append(decimals).toString();
- }
-
}
diff --git a/jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java b/jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java
new file mode 100644
index 00000000000..394f87c07cc
--- /dev/null
+++ b/jdisc_http_service/src/main/java/com/yahoo/container/logging/JsonConnectionLogWriter.java
@@ -0,0 +1,117 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.logging;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author bjorncs
+ */
+class JsonConnectionLogWriter implements LogWriter<ConnectionLogEntry> {
+
+ private final JsonFactory jsonFactory = new JsonFactory(new ObjectMapper());
+
+ @Override
+ public void write(ConnectionLogEntry record, OutputStream outputStream) throws IOException {
+ try (JsonGenerator generator = createJsonGenerator(outputStream)) {
+ generator.writeStartObject();
+ generator.writeStringField("id", record.id());
+ generator.writeStringField("timestamp", record.timestamp().toString());
+
+ writeOptionalSeconds(generator, "duration", unwrap(record.durationSeconds()));
+ writeOptionalString(generator, "peerAddress", unwrap(record.peerAddress()));
+ writeOptionalInteger(generator, "peerPort", unwrap(record.peerPort()));
+ writeOptionalString(generator, "localAddress", unwrap(record.localAddress()));
+ writeOptionalInteger(generator, "localPort", unwrap(record.localPort()));
+ writeOptionalString(generator, "remoteAddress", unwrap(record.remoteAddress()));
+ writeOptionalInteger(generator, "remotePort", unwrap(record.remotePort()));
+ writeOptionalLong(generator, "httpBytesReceived", unwrap(record.httpBytesReceived()));
+ writeOptionalLong(generator, "httpBytesSent", unwrap(record.httpBytesSent()));
+ writeOptionalLong(generator, "requests", unwrap(record.requests()));
+ writeOptionalLong(generator, "responses", unwrap(record.responses()));
+
+ String sslProtocol = unwrap(record.sslProtocol());
+ String sslSessionId = unwrap(record.sslSessionId());
+ String sslCipherSuite = unwrap(record.sslCipherSuite());
+ String sslPeerSubject = unwrap(record.sslPeerSubject());
+ Instant sslPeerNotBefore = unwrap(record.sslPeerNotBefore());
+ Instant sslPeerNotAfter = unwrap(record.sslPeerNotAfter());
+ String sslSniServerName = unwrap(record.sslSniServerName());
+ String sslHandshakeFailureException = unwrap(record.sslHandshakeFailureException());
+ String sslHandshakeFailureMessage = unwrap(record.sslHandshakeFailureMessage());
+ String sslHandshakeFailureType = unwrap(record.sslHandshakeFailureType());
+
+ if (isAnyValuePresent(
+ sslProtocol, sslSessionId, sslCipherSuite, sslPeerSubject, sslPeerNotBefore, sslPeerNotAfter,
+ sslSniServerName, sslHandshakeFailureException, sslHandshakeFailureMessage, sslHandshakeFailureType)) {
+ generator.writeObjectFieldStart("ssl");
+
+ writeOptionalString(generator, "protocol", sslProtocol);
+ writeOptionalString(generator, "sessionId", sslSessionId);
+ writeOptionalString(generator, "cipherSuite", sslCipherSuite);
+ writeOptionalString(generator, "peerSubject", sslPeerSubject);
+ writeOptionalTimestamp(generator, "peerNotBefore", sslPeerNotBefore);
+ writeOptionalTimestamp(generator, "peerNotAfter", sslPeerNotAfter);
+ writeOptionalString(generator, "sniServerName", sslSniServerName);
+
+ if (isAnyValuePresent(sslHandshakeFailureException, sslHandshakeFailureMessage, sslHandshakeFailureType)) {
+ generator.writeObjectFieldStart("handshake-failure");
+ writeOptionalString(generator, "exception", sslHandshakeFailureException);
+ writeOptionalString(generator, "message", sslHandshakeFailureMessage);
+ writeOptionalString(generator, "type", sslHandshakeFailureType);
+ generator.writeEndObject();
+ }
+
+ generator.writeEndObject();
+ }
+ }
+ }
+
+ private void writeOptionalString(JsonGenerator generator, String name, String value) throws IOException {
+ if (value != null) {
+ generator.writeStringField(name, value);
+ }
+ }
+
+ private void writeOptionalInteger(JsonGenerator generator, String name, Integer value) throws IOException {
+ if (value != null) {
+ generator.writeNumberField(name, value);
+ }
+ }
+
+ private void writeOptionalLong(JsonGenerator generator, String name, Long value) throws IOException {
+ if (value != null) {
+ generator.writeNumberField(name, value);
+ }
+ }
+
+ private void writeOptionalTimestamp(JsonGenerator generator, String name, Instant value) throws IOException {
+ if (value != null) {
+ generator.writeStringField(name, value.toString());
+ }
+ }
+
+ private void writeOptionalSeconds(JsonGenerator generator, String name, Double value) throws IOException {
+ if (value != null) {
+ FormatUtil.writeSecondsField(generator, name, value);
+ }
+ }
+
+ private static boolean isAnyValuePresent(Object... values) { return Arrays.stream(values).anyMatch(Objects::nonNull); }
+ private static <T> T unwrap(Optional<T> maybeValue) { return maybeValue.orElse(null); }
+
+ private JsonGenerator createJsonGenerator(OutputStream outputStream) throws IOException {
+ return jsonFactory.createGenerator(outputStream, JsonEncoding.UTF8)
+ .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false)
+ .configure(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM, false);
+ }
+}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/container/logging/ConnectionLogEntryTest.java b/jdisc_http_service/src/test/java/com/yahoo/container/logging/ConnectionLogEntryTest.java
deleted file mode 100644
index fbf9bd1dc23..00000000000
--- a/jdisc_http_service/src/test/java/com/yahoo/container/logging/ConnectionLogEntryTest.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.container.logging;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.time.Instant;
-import java.util.UUID;
-
-/**
- * @author mortent
- */
-public class ConnectionLogEntryTest {
-
- @Test
- public void test_serialization () throws IOException {
- var id = UUID.randomUUID();
- var instant = Instant.parse("2021-01-13T12:12:12Z");
- ConnectionLogEntry entry = ConnectionLogEntry.builder(id, instant)
- .withPeerPort(1234)
- .build();
-
- String expected = "{" +
- "\"id\":\""+id.toString()+"\"," +
- "\"timestamp\":\"2021-01-13T12:12:12Z\"," +
- "\"peerPort\":1234" +
- "}";
- Assert.assertEquals(expected, entry.toJson());
- }
-}
diff --git a/jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java b/jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java
new file mode 100644
index 00000000000..b8978fe489c
--- /dev/null
+++ b/jdisc_http_service/src/test/java/com/yahoo/container/logging/JsonConnectionLogWriterTest.java
@@ -0,0 +1,36 @@
+package com.yahoo.container.logging;// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+import com.yahoo.test.json.JsonTestHelper;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.UUID;
+
+/**
+ * @author bjorncs
+ */
+class JsonConnectionLogWriterTest {
+
+ @Test
+ void test_serialization() throws IOException {
+ var id = UUID.randomUUID();
+ var instant = Instant.parse("2021-01-13T12:12:12Z");
+ ConnectionLogEntry entry = ConnectionLogEntry.builder(id, instant)
+ .withPeerPort(1234)
+ .build();
+ String expectedJson = "{" +
+ "\"id\":\""+id.toString()+"\"," +
+ "\"timestamp\":\"2021-01-13T12:12:12Z\"," +
+ "\"peerPort\":1234" +
+ "}";
+
+ JsonConnectionLogWriter writer = new JsonConnectionLogWriter();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ writer.write(entry, out);
+ String actualJson = out.toString(StandardCharsets.UTF_8);
+ JsonTestHelper.assertJsonEquals(actualJson, expectedJson);
+ }
+} \ No newline at end of file