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;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022import java.util.concurrent.*;
023import java.util.logging.*;
024
025/**
026 * {@link LogBackend} implementation for {@link java.util.logging java.util.logging} (JUL).
027 *
028 * <p>
029 * This is the default backend driven by {@link LoggersManager} when no {@link LoggersSettings} declares another
030 * one, and it carries the original v1 {@code /loggers} behavior so the JUL path is unchanged: reads report each
031 * logger's own ({@link Logger#getLevel() configured}) level (empty string when inherited), the root logger is
032 * keyed {@code "ROOT"}, and sets are process-lifetime-only via {@link Logger#setLevel(Level)}.
033 *
034 * @since 10.0.0
035 */
036@SuppressWarnings({
037   "java:S6548" // Singleton is appropriate for a JUL logging backend — single shared instance per JVM.
038})
039public class JulLogBackend implements LogBackend {
040
041   /** Process-wide shared instance. */
042   public static final JulLogBackend INSTANCE = new JulLogBackend();
043
044   // Strong references to loggers whose levels we've explicitly set.  The JUL LogManager holds only weak
045   // references to loggers, so a logger with no other strong referent can be garbage-collected between a
046   // set and a subsequent read — silently dropping the runtime-configured level (the endpoint's set is
047   // documented as process-lifetime-only) and causing reads to 404.  Pinning here keeps set levels effective.
048   private final Set<Logger> pinned = ConcurrentHashMap.newKeySet();
049
050   private JulLogBackend() {}
051
052   @Override /* LogBackend */
053   public Map<String,String> getLevels() {
054      var out = new TreeMap<String,String>();
055      var lm = LogManager.getLogManager();
056      var names = Collections.list(lm.getLoggerNames());
057      for (var name : names) {
058         var logger = lm.getLogger(name);
059         if (logger == null)
060            continue;
061         var level = logger.getLevel();
062         var key = name.isEmpty() ? "ROOT" : name;
063         out.put(key, level == null ? "" : level.getName());
064      }
065      return out;
066   }
067
068   @Override /* LogBackend */
069   public String getLevel(String name) {
070      var logger = LogManager.getLogManager().getLogger(resolveName(name));
071      if (logger == null)
072         return null;
073      var level = logger.getLevel();
074      return level == null ? "" : level.getName();
075   }
076
077   @Override /* LogBackend */
078   public void setLevel(String name, String level) {
079      var logger = Logger.getLogger(resolveName(name));
080      logger.setLevel(level == null || level.isBlank() ? null : Level.parse(level.trim()));
081      pinned.add(logger);
082   }
083
084   private static String resolveName(String name) {
085      // The root logger is the empty-string-named logger; expose it under the friendlier "ROOT" alias.
086      return (name == null || eq(name, "ROOT")) ? "" : name;
087   }
088}