aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java
diff options
context:
space:
mode:
Diffstat (limited to 'container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java')
-rw-r--r--container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java193
1 files changed, 193 insertions, 0 deletions
diff --git a/container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java b/container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java
new file mode 100644
index 00000000000..680ee5acbd9
--- /dev/null
+++ b/container-core/src/main/java/com/yahoo/container/logging/JSONFormatter.java
@@ -0,0 +1,193 @@
+// Copyright 2017 Yahoo Holdings. 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 com.yahoo.yolean.trace.TraceNode;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Formatting of an {@link AccessLogEntry} in the Vespa JSON access log format.
+ *
+ * @author frodelu
+ */
+public class JSONFormatter implements LogWriter<RequestLogEntry> {
+ private static final String COVERAGE = "coverage";
+ private static final String COVERAGE_COVERAGE = "coverage";
+ private static final String COVERAGE_DOCUMENTS = "documents";
+ private static final String COVERAGE_DEGRADE = "degraded";
+ private static final String COVERAGE_DEGRADE_MATCHPHASE = "match-phase";
+ private static final String COVERAGE_DEGRADE_TIMEOUT = "timeout";
+ private static final String COVERAGE_DEGRADE_ADAPTIVE_TIMEOUT = "adaptive-timeout";
+ private static final String COVERAGE_DEGRADED_NON_IDEAL_STATE = "non-ideal-state";
+
+ private final JsonFactory generatorFactory;
+
+ private static Logger logger = Logger.getLogger(JSONFormatter.class.getName());
+
+ public JSONFormatter() {
+ generatorFactory = new JsonFactory(new ObjectMapper());
+ }
+
+ @Override
+ public void write(RequestLogEntry entry, OutputStream outputStream) throws IOException {
+ try (JsonGenerator generator = createJsonGenerator(outputStream)){
+ generator.writeStartObject();
+ String peerAddress = entry.peerAddress().get();
+ generator.writeStringField("ip", peerAddress);
+ long time = entry.timestamp().get().toEpochMilli();
+ 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(""));
+ generator.writeStringField("uri", getNormalizedURI(entry.rawPath().orElse(null), entry.rawQuery().orElse(null)));
+ generator.writeStringField("version", entry.httpVersion().orElse(""));
+ generator.writeStringField("agent", entry.userAgent().orElse(""));
+ generator.writeStringField("host", entry.hostString().orElse(""));
+ generator.writeStringField("scheme", entry.scheme().orElse(null));
+ generator.writeNumberField("localport", entry.localPort().getAsInt());
+
+ String connectionId = entry.connectionId().orElse(null);
+ if (connectionId != null) {
+ generator.writeStringField("connection", connectionId);
+ }
+
+ Principal userPrincipal = entry.userPrincipal().orElse(null);
+ if (userPrincipal != null) {
+ generator.writeStringField("user-principal", userPrincipal.getName());
+ }
+
+ Principal sslPrincipal = entry.sslPrincipal().orElse(null);
+ if (sslPrincipal != null) {
+ generator.writeStringField("ssl-principal", sslPrincipal.getName());
+ }
+
+ String remoteAddress = entry.remoteAddress().orElse(null);
+ int remotePort = entry.remotePort().orElse(0);
+ // Only add remote address/port fields if relevant
+ if (remoteAddressDiffers(peerAddress, remoteAddress)) {
+ generator.writeStringField("remoteaddr", remoteAddress);
+ if (remotePort > 0) {
+ generator.writeNumberField("remoteport", remotePort);
+ }
+ }
+
+ // Only add peer address/port fields if relevant
+ if (peerAddress != null) {
+ generator.writeStringField("peeraddr", peerAddress);
+
+ int peerPort = entry.peerPort().getAsInt();
+ if (peerPort > 0 && peerPort != remotePort) {
+ generator.writeNumberField("peerport", peerPort);
+ }
+ }
+
+ TraceNode trace = entry.traceNode().orElse(null);
+ if (trace != null) {
+ long timestamp = trace.timestamp();
+ if (timestamp == 0L) {
+ timestamp = time;
+ }
+ trace.accept(new TraceRenderer(generator, timestamp));
+ }
+
+ // Only add search sub block of this is a search request
+ if (isSearchRequest(entry)) {
+ HitCounts hitCounts = entry.hitCounts().get();
+ generator.writeObjectFieldStart("search");
+ generator.writeNumberField("totalhits", getTotalHitCount(hitCounts));
+ generator.writeNumberField("hits", getRetrievedHitCount(hitCounts));
+ Coverage c = hitCounts.getCoverage();
+ if (c != null) {
+ generator.writeObjectFieldStart(COVERAGE);
+ generator.writeNumberField(COVERAGE_COVERAGE, c.getResultPercentage());
+ generator.writeNumberField(COVERAGE_DOCUMENTS, c.getDocs());
+ if (c.isDegraded()) {
+ generator.writeObjectFieldStart(COVERAGE_DEGRADE);
+ if (c.isDegradedByMatchPhase())
+ generator.writeBooleanField(COVERAGE_DEGRADE_MATCHPHASE, c.isDegradedByMatchPhase());
+ if (c.isDegradedByTimeout())
+ generator.writeBooleanField(COVERAGE_DEGRADE_TIMEOUT, c.isDegradedByTimeout());
+ if (c.isDegradedByAdapativeTimeout())
+ generator.writeBooleanField(COVERAGE_DEGRADE_ADAPTIVE_TIMEOUT, c.isDegradedByAdapativeTimeout());
+ if (c.isDegradedByNonIdealState())
+ generator.writeBooleanField(COVERAGE_DEGRADED_NON_IDEAL_STATE, c.isDegradedByNonIdealState());
+ generator.writeEndObject();
+ }
+ generator.writeEndObject();
+ }
+ generator.writeEndObject();
+ }
+
+ // Add key/value access log entries. Keys with single values are written as single
+ // string value fields while keys with multiple values are written as string arrays
+ Collection<String> keys = entry.extraAttributeKeys();
+ if (!keys.isEmpty()) {
+ generator.writeObjectFieldStart("attributes");
+ for (String key : keys) {
+ Collection<String> values = entry.extraAttributeValues(key);
+ if (values.size() == 1) {
+ generator.writeStringField(key, values.iterator().next());
+ } else {
+ generator.writeFieldName(key);
+ generator.writeStartArray();
+ for (String s : values) {
+ generator.writeString(s);
+ }
+ generator.writeEndArray();
+ }
+ }
+ generator.writeEndObject();
+ }
+
+ generator.writeEndObject();
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "Unable to generate JSON access log entry: " + e.getMessage(), e);
+ }
+ }
+
+ private JsonGenerator createJsonGenerator(OutputStream outputStream) throws IOException {
+ return generatorFactory.createGenerator(outputStream, JsonEncoding.UTF8)
+ .configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false)
+ .configure(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM, false);
+ }
+
+ private boolean remoteAddressDiffers(String ipV4Address, String remoteAddress) {
+ return remoteAddress != null && !Objects.equals(ipV4Address, remoteAddress);
+ }
+
+ private boolean isSearchRequest(RequestLogEntry entry) {
+ return entry != null && entry.hitCounts().isPresent();
+ }
+
+ private long getTotalHitCount(HitCounts counts) {
+ if (counts == null) {
+ return 0;
+ }
+
+ return counts.getTotalHitCount();
+ }
+
+ private int getRetrievedHitCount(HitCounts counts) {
+ if (counts == null) {
+ return 0;
+ }
+
+ return counts.getRetrievedHitCount();
+ }
+
+ private static String getNormalizedURI(String rawPath, String rawQuery) {
+ if (rawPath == null) return null;
+ return rawQuery != null ? rawPath + "?" + rawQuery : rawPath;
+ }
+}