Java: как зарегистрировать raw JSON как JSON и избежать экранирования во время ведения журнала с logback / slf4j
Я использую SLF4J с Logback в приложении JAX-RS... Я хочу войти в JSON таким образом, чтобы мое сообщение не кодировалось снова, а печаталось raw в файл журнала:
на данный момент это выглядит так:
{"@timestamp":1363834123012,"@message":"{"text":"From MLK to Barack 
Ob...n"}"
но я хочу иметь это:
  {"@timestamp":1363834123012,"@message": { "text ": "From MLK to Barack 
Ob...n}
причина в том, что я хочу снова проанализировать JSON и хочу избежать unescaping данных.
Я написал пользовательский кодировщик logback, но я не нашел способа избежать побега. Могу ли я передать объект в logback и изменить настройки в зависимости от типа объекта?
Edit: я нашел способ-не совсем элегантный - как просил SSCE:
в моем приложении
// SLF4J Logger
private static Logger logger = LoggerFactory.getLogger(MyClass.class);
// A logback? Marker
private Marker foo = MarkerFactory.getMarker("foo");
// Jackson ObjectMapper()
ObjectMapper mapper = new ObjectMapper();
// Log something... 
logger.info(foo, mapper.writeValueAsString(json));
я использовал вариант Logstash-Encoder, найденный здесь:https://github.com/logstash/logstash-logback-encoder
package my.package;
import static org.apache.commons.io.IOUtils.*;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import org.codehaus.jackson.JsonGenerator.Feature;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;
import org.slf4j.Marker;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.ThrowableProxyUtil;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.encoder.EncoderBase;
public class JsonEncoder extends EncoderBase<ILoggingEvent> {
    private static final ObjectMapper MAPPER = new ObjectMapper().configure(
        Feature.ESCAPE_NON_ASCII, true);
    private static Marker M;
    private boolean immediateFlush = true;
@Override
public void doEncode(ILoggingEvent event) throws IOException {
    M = event.getMarker();
    ObjectNode eventNode = MAPPER.createObjectNode();
    eventNode.put("@timestamp", event.getTimeStamp());
    //
    if (M != null) {
        if (M.getName().equals("foo")) {
            JsonNode j = MAPPER.readTree(event.getFormattedMessage());
            eventNode.put("@foo", j);
        }
    } else {
        eventNode.put("@message", event.getFormattedMessage());
    }
    eventNode.put("@fields", createFields(event));
    write(MAPPER.writeValueAsBytes(eventNode), outputStream);
    write(CoreConstants.LINE_SEPARATOR, outputStream);
    if (immediateFlush) {
        outputStream.flush();
    }
}
private ObjectNode createFields(ILoggingEvent event) {
         // not important here
    return fieldsNode;
}
@Override
public void close() throws IOException {
    write(LINE_SEPARATOR, outputStream);
}
public boolean isImmediateFlush() {
    return immediateFlush;
}
public void setImmediateFlush(boolean immediateFlush) {
    this.immediateFlush = immediateFlush;
}
}
это работает сейчас! Да! Но я думаю, что это не лучший способ сделать это (сериализовать, десериализовать JSON...)
7 ответов
Logback не делает ничего необычного с JSON. Это просто строка, которая регистрируется как обычно. Побег, вероятно, происходит на вашем конце, если вы не говорите о каком-то приложении JSON, которое записывает его в этом формате. Я уверен, что Logback сам по себе не имеет ничего подобного, поэтому вы хотели бы посмотреть, откуда у вас есть Appender, если это ваша проблема. Ан SSCCE поможет в дальнейшем устранении неполадок.
Если у вас есть сообщения в формате Json, верхние решения работают, но не так хорошо, так как вы не хотите вызывать конкретный код logstash, каждый раз, когда вы используете свой регистратор в коде.
просто добавив
net.logstash.logback.encoder.LogstashEncoder
недостаточно, так как сообщение itsself остается убежал. Чтобы решить эту проблему, попробуйте следующее в logback.XML-код:
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
     <providers>
        <timestamp/>
        <version/>
        <loggerName/>
        <pattern>
            <pattern>
                {
                "jsonMessage": "#asJson{%message}"
                }
            </pattern>
        </pattern>
    </providers>
</encoder>
шаблон #asJson будет unescape ваше сообщение.
я столкнулся с такой же проблемой. Я решил это с
<encoder
 class="net.logstash.logback.encoder.LogstashEncoder">          
</encoder
вместо
<encoder
 class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
в моем коде java я использовал:
SRV_PERF_LOGGER.info(net.logstash.logback.marker.Markers.appendRaw("message", jackson.writeValueAsString(dto)), null);
вот обновленный (2016) Groovy logback config, который сбрасывает ваши журналы в формате json в файл и строки отладки в консоли. Мне потребовался весь день, чтобы понять, поэтому я подумал, что обновлю поток.
    import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.core.ConsoleAppender
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy
import ch.qos.logback.core.rolling.RollingFileAppender
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy
import net.logstash.logback.encoder.LogstashEncoder
import static ch.qos.logback.classic.Level.INFO
import static ch.qos.logback.classic.Level.WARN
def PROJECT_ID = "com.foo"
    appender("file", RollingFileAppender) {
        file = "/tmp/logs/${PROJECT_ID}.json"
        encoder(LogstashEncoder)
        rollingPolicy(FixedWindowRollingPolicy) {
            maxIndex = 1
            fileNamePattern = "logs/${PROJECT_ID}.json.%i"
        }
        triggeringPolicy(SizeBasedTriggeringPolicy) {
            maxFileSize = "1MB"
        }
    }
    appender("STDOUT", ConsoleAppender) {
        encoder(PatternLayoutEncoder) {
            pattern = "%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"
        }
    }
    logger("com.foo", INFO, ["STDOUT", "file"], false)
    root(WARN, ["STDOUT", "file"])
используйте RawJsonAppendingMarker:
log.trace(net.logstash.logback.marker.Markers.appendRaw("jsonMessage", jsonString), null);
просто пришел на это сам и нашел статью с несколько рекомендаций по заготовке леса.
если вы используете maven, поместите эту зависимость в pom.xml
<dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>3.4</version>
</dependency>
и поставить что-то вроде этого logback.xml
<configuration>
    <property name="PROJECT_ID" value="example"/>
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>logs/${PROJECT_ID}.json</File>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <maxIndex>1</maxIndex>
            <FileNamePattern>logs/${PROJECT_ID}.json.%i</FileNamePattern>
        </rollingPolicy>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>1MB</MaxFileSize>
        </triggeringPolicy>
    </appender>
    <logger name="eu.kielczewski" additivity="false" level="INFO">
        <appender-ref ref="file"/>
    </logger>
    <root level="WARN">
        <appender-ref ref="file"/>
    </root>
</configuration>
создает  под logs/. Файл сворачивается один раз, когда он достигает размера 1 МБ.
LOGGER.debug(append("object", someObject), "log message");
Я не вижу исходного кода, который вызывает вашу проблему, но я подозреваю, что это может выглядеть так
JsonNode logOutput;
String messageJSONAsString;
...
logOutput.put("@message", messageJSONAsString);
logger.info(objectMapper.writeValueAsString(logOutput);
это произведет экранированный JSON в вашем выходе, потому что, когда вы поместите сообщение в выходной Jsonode, Jackson повторно экранирует его для вас, чтобы убедиться, что выход действителен JSON.
решение здесь состоит в том, чтобы поместить сообщение в Вывод Как ObjectNode, а не как строку. Обычно у вас уже есть доступ к объект как объект, в этом случае вы можете сделать
ObjectNode jsonObject = objectMapper.valueToTree(messageObject);
logOutput.put("@message", jsonObject)
в противном случае, если ваше сообщение является строкой JSON, проанализируйте его и добавьте в вывод
logoutput.put("@message", objectMapper.readTree(messageJSONAsString));