001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.management.logging;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.rest.server.management.*;
024import org.apache.logging.log4j.*;
025import org.apache.logging.log4j.core.*;
026import org.apache.logging.log4j.core.config.*;
027
028/**
029 * {@link LogBackend} adapter for <a class="doclink" href="https://logging.apache.org/log4j/2.x/">Apache Log4j 2</a>,
030 * for use behind the {@code /loggers} management endpoint.
031 *
032 * <p>
033 * Drives the concrete Log4j2 {@link LoggerContext} / {@link Configuration} &mdash; <b>not</b> SLF4J the facade
034 * (an SLF4J&rarr;Log4j2 binding still routes here because the emitting backend is Log4j2). Register it explicitly:
035 *
036 * <p class='bjava'>
037 *    <ja>@Bean</ja>
038 *    <jk>public</jk> LoggersSettings loggersSettings() {
039 *       <jk>return</jk> LoggersSettings.<jsm>create</jsm>().enableWrite().backend(<jk>new</jk> Log4j2LogBackend()).build();
040 *    }
041 * </p>
042 *
043 * <h5 class='topic'>Level mapping</h5>
044 *
045 * <p>
046 * Levels are Log4j2 standard level names ({@code "TRACE"}, {@code "DEBUG"}, {@code "INFO"}, {@code "WARN"},
047 * {@code "ERROR"}, {@code "FATAL"}, {@code "OFF"}, {@code "ALL"}). Reads report each {@link LoggerConfig}'s
048 * <b>configured</b> level. Sets are process-lifetime-only ({@link Configurator#setLevel(String, Level)}); the
049 * root logger is addressed as {@code "ROOT"}.
050 *
051 * <p>
052 * Log4j2 has no "inherited / no own level" sentinel the way JUL/Logback do &mdash; every {@link LoggerConfig} in
053 * the configuration carries a concrete level &mdash; so reads return a level name (never the empty-string
054 * "inherited" marker) for configured loggers, and {@code null} for a name that has no {@code LoggerConfig}.
055 *
056 * @since 10.0.0
057 */
058@SuppressWarnings({
059   "resource" // The LoggerContext (AutoCloseable) is the shared Log4j2 context owned by LogManager, not by this backend
060})
061public class Log4j2LogBackend implements LogBackend {
062
063   private final LoggerContext context;
064
065   /**
066    * Constructor using the current Log4j2 {@link LoggerContext}.
067    */
068   public Log4j2LogBackend() {
069      this((LoggerContext) LogManager.getContext(false));
070   }
071
072   /**
073    * Constructor with an explicit {@link LoggerContext} (for tests / multi-context setups).
074    *
075    * @param context The Log4j2 logger context. Must not be <jk>null</jk>.
076    */
077   public Log4j2LogBackend(LoggerContext context) {
078      this.context = Objects.requireNonNull(context, "context");
079   }
080
081   @Override /* LogBackend */
082   public Map<String,String> getLevels() {
083      var out = new TreeMap<String,String>();
084      var cfg = context.getConfiguration();
085      // The root LoggerConfig is NOT in getLoggers() (that map holds only the named, explicitly-configured
086      // loggers) - it's reachable via getRootLogger().  Surface it under the "ROOT" key.
087      var root = cfg.getRootLogger();
088      if (root != null)
089         out.put("ROOT", root.getLevel() == null ? "" : root.getLevel().name());
090      for (var lc : cfg.getLoggers().values()) {
091         if (lc.getName().isEmpty())
092            continue;  // defensive: root already handled above
093         out.put(lc.getName(), lc.getLevel() == null ? "" : lc.getLevel().name());
094      }
095      return out;
096   }
097
098   @Override /* LogBackend */
099   public String getLevel(String name) {
100      var cfg = context.getConfiguration();
101      var resolved = resolveName(name);
102      var lc = resolved.isEmpty() ? cfg.getRootLogger() : cfg.getLoggers().get(resolved);
103      if (lc == null)
104         return null;
105      var level = lc.getLevel();
106      return level == null ? "" : level.name();
107   }
108
109   @Override /* LogBackend */
110   public void setLevel(String name, String level) {
111      var target = (name == null || eq(name, "ROOT")) ? LogManager.ROOT_LOGGER_NAME : name;
112      // Null/blank clears the override back to the parent's level (Configurator treats null as "remove override").
113      Configurator.setLevel(target, level == null || level.isBlank() ? null : parse(level.trim()));
114   }
115
116   private static Level parse(String level) {
117      // Level.getLevel(name) returns null for an unknown name (Level.toLevel would silently default) - hard-fail
118      // to match the JUL backend's IllegalArgumentException contract.
119      var l = Level.getLevel(level.toUpperCase(Locale.ROOT));
120      if (l == null)
121         throw iaex("Not a valid Log4j2 level: '%s'.", level);
122      return l;
123   }
124
125   private static String resolveName(String name) {
126      return (name == null || eq(name, "ROOT")) ? "" : name;
127   }
128}