// 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.ByteArrayOutputStream;
import java.io.IOException;
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 {
    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());
    }

    /**
     * The main method for formatting the associated {@link RequestLogEntry} as a Vespa JSON access log string
     *
     * @return The Vespa JSON access log string without trailing newline
     */
    public String format(RequestLogEntry entry) {
        ByteArrayOutputStream logLine = new ByteArrayOutputStream();
        try {
            JsonGenerator generator = generatorFactory.createGenerator(logLine, JsonEncoding.UTF8);
            generator.writeStartObject();
            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());
            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 principal = entry.userPrincipal().orElse(null);
            if (principal != null) {
                generator.writeStringField("user-principal", principal.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();
            generator.close();

        } catch (IOException e) {
            logger.log(Level.WARNING, "Unable to generate JSON access log entry: " + e.getMessage());
        }

        return logLine.toString();
    }


    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;
    }

    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();
    }

}

@@ -7699,24 +7699,27 @@
 getMessage()
+, e
 );%0A        %7D

Include stack trace in log