/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.common.logging; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.Node; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginAttribute; import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; import org.apache.logging.log4j.core.config.plugins.PluginFactory; import org.apache.logging.log4j.core.layout.AbstractStringLayout; import org.apache.logging.log4j.core.layout.ByteBufferDestination; import org.apache.logging.log4j.core.layout.PatternLayout; import org.opensearch.core.common.Strings; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Formats log events as strings in a json format. *

* The class is wrapping the {@link PatternLayout} with a pattern to format into json. This gives more flexibility and control over how the * log messages are formatted in {@link org.apache.logging.log4j.core.layout.JsonLayout} * There are fields which are always present in the log line: *

*

* It is possible to add more or override them with opensearchmessagefield * * appender.logger.layout.opensearchmessagefields=message,took,took_millis,total_hits,types,stats,search_type,total_shards,source,id * * Each of these will be expanded into a json field with a value taken {@link OpenSearchLogMessage} field. In the example above * ... "message": %OpenSearchMessageField{message}, "took": %OpenSearchMessageField{took} ... * the message passed to a logger will be overriden with a value from %OpenSearchMessageField{message} *

* The value taken from %OpenSearchMessageField{message} has to be a simple escaped JSON value and is populated in subclasses of * OpenSearchLogMessage *

* The message field is truncated at 10000 characters by default. This limit can be controlled by setting the * appender.logger.layout.maxmessagelength to the desired limit or to 0 to disable truncation. * * @opensearch.internal */ @Plugin(name = "OpenSearchJsonLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true) public class OpenSearchJsonLayout extends AbstractStringLayout { private final PatternLayout patternLayout; protected OpenSearchJsonLayout( String typeName, Charset charset, String[] opensearchMessageFields, int maxMessageLength, Configuration configuration ) { super(charset); this.patternLayout = PatternLayout.newBuilder() .withPattern(pattern(typeName, opensearchMessageFields, maxMessageLength)) .withAlwaysWriteExceptions(false) .withConfiguration(configuration) .build(); } private String pattern(String type, String[] opensearchMessageFields, int maxMessageLength) { if (Strings.isEmpty(type)) { throw new IllegalArgumentException("layout parameter 'type_name' cannot be empty"); } String messageFormat = "%m"; if (maxMessageLength < 0) { throw new IllegalArgumentException("layout parameter 'maxmessagelength' cannot be a negative number"); } else if (maxMessageLength > 0) { messageFormat = "%.-" + Integer.toString(maxMessageLength) + "m"; } Map map = new LinkedHashMap<>(); map.put("type", inQuotes(type)); map.put("timestamp", inQuotes("%d{yyyy-MM-dd'T'HH:mm:ss,SSSZZ}")); map.put("level", inQuotes("%p")); map.put("component", inQuotes("%c{1.}")); map.put("cluster.name", inQuotes("${sys:opensearch.logs.cluster_name}")); map.put("node.name", inQuotes("%node_name")); map.put("message", inQuotes("%notEmpty{%enc{%marker}{JSON} }%enc{" + messageFormat + "}{JSON}")); for (String key : opensearchMessageFields) { map.put(key, inQuotes("%OpenSearchMessageField{" + key + "}")); } return createPattern(map, Stream.of(opensearchMessageFields).collect(Collectors.toSet())); } private String createPattern(Map map, Set opensearchMessageFields) { StringBuilder sb = new StringBuilder(); sb.append("{"); String separator = ""; for (Map.Entry entry : map.entrySet()) { if (opensearchMessageFields.contains(entry.getKey())) { sb.append("%notEmpty{"); sb.append(separator); appendField(sb, entry); sb.append("}"); } else { sb.append(separator); appendField(sb, entry); } separator = ", "; } sb.append(notEmpty(", %node_and_cluster_id ")); sb.append("%exceptionAsJson "); sb.append("}"); sb.append(System.lineSeparator()); return sb.toString(); } private void appendField(StringBuilder sb, Map.Entry entry) { sb.append(jsonKey(entry.getKey())); sb.append(entry.getValue().toString()); } private String notEmpty(String value) { return "%notEmpty{" + value + "}"; } private CharSequence jsonKey(String s) { return inQuotes(s) + ": "; } private String inQuotes(String s) { return "\"" + s + "\""; } @PluginFactory public static OpenSearchJsonLayout createLayout( String type, Charset charset, String[] opensearchmessagefields, int maxMessageLength, Configuration configuration ) { return new OpenSearchJsonLayout(type, charset, opensearchmessagefields, maxMessageLength, configuration); } PatternLayout getPatternLayout() { return patternLayout; } /** * Builder for a json layout * * @opensearch.internal */ public static class Builder> extends AbstractStringLayout.Builder implements org.apache.logging.log4j.core.util.Builder { @PluginAttribute("type_name") String type; @PluginAttribute(value = "charset", defaultString = "UTF-8") Charset charset; @PluginAttribute("opensearchmessagefields") private String opensearchMessageFields; @PluginAttribute(value = "maxmessagelength", defaultInt = 10000) private int maxMessageLength; @PluginConfiguration private Configuration configuration; public Builder() { setCharset(StandardCharsets.UTF_8); setMaxMessageLength(10000); } @Override public OpenSearchJsonLayout build() { String[] split = Strings.isNullOrEmpty(opensearchMessageFields) ? new String[] {} : opensearchMessageFields.split(","); return OpenSearchJsonLayout.createLayout(type, charset, split, maxMessageLength, configuration); } public Charset getCharset() { return charset; } public B setCharset(final Charset charset) { this.charset = charset; return asBuilder(); } public String getType() { return type; } public B setType(final String type) { this.type = type; return asBuilder(); } public String getOpenSearchMessageFields() { return opensearchMessageFields; } public B setOpenSearchMessageFields(String opensearchMessageFields) { this.opensearchMessageFields = opensearchMessageFields; return asBuilder(); } public int getMaxMessageLength() { return maxMessageLength; } public B setMaxMessageLength(final int maxMessageLength) { this.maxMessageLength = maxMessageLength; return asBuilder(); } public Configuration getConfiguration() { return configuration; } public B setConfiguration(final Configuration configuration) { this.configuration = configuration; return asBuilder(); } } @PluginBuilderFactory public static > B newBuilder() { return new OpenSearchJsonLayout.Builder().asBuilder(); } @Override public String toSerializable(final LogEvent event) { return patternLayout.toSerializable(event); } @Override public Map getContentFormat() { return patternLayout.getContentFormat(); } @Override public void encode(final LogEvent event, final ByteBufferDestination destination) { patternLayout.encode(event, destination); } @Override public String toString() { final StringBuilder sb = new StringBuilder("OpenSearchJsonLayout{"); sb.append("patternLayout=").append(patternLayout); sb.append('}'); return sb.toString(); } }