/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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. */ package org.apache.logging.log4j.core.jmx; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.io.StringWriter; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicLong; import javax.management.MBeanNotificationInfo; import javax.management.Notification; import javax.management.NotificationBroadcasterSupport; import javax.management.ObjectName; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.util.Closer; import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.Strings; /** * Implementation of the {@code LoggerContextAdminMBean} interface. */ public class LoggerContextAdmin extends NotificationBroadcasterSupport implements LoggerContextAdminMBean, PropertyChangeListener { private static final int PAGE = 4 * 1024; private static final int TEXT_BUFFER = 64 * 1024; private static final int BUFFER_SIZE = 2048; private static final StatusLogger LOGGER = StatusLogger.getLogger(); private final AtomicLong sequenceNo = new AtomicLong(); private final ObjectName objectName; private final LoggerContext loggerContext; /** * Constructs a new {@code LoggerContextAdmin} with the {@code Executor} to be used for sending {@code Notification} * s asynchronously to listeners. * * @param executor used to send notifications asynchronously * @param loggerContext the instrumented object */ public LoggerContextAdmin(final LoggerContext loggerContext, final Executor executor) { super(executor, createNotificationInfo()); this.loggerContext = Objects.requireNonNull(loggerContext, "loggerContext"); try { final String ctxName = Server.escape(loggerContext.getName()); final String name = String.format(PATTERN, ctxName); objectName = new ObjectName(name); } catch (final Exception e) { throw new IllegalStateException(e); } loggerContext.addPropertyChangeListener(this); } private static MBeanNotificationInfo createNotificationInfo() { final String[] notifTypes = new String[] { NOTIF_TYPE_RECONFIGURED }; final String name = Notification.class.getName(); final String description = "Configuration reconfigured"; return new MBeanNotificationInfo(notifTypes, name, description); } @Override public String getStatus() { return loggerContext.getState().toString(); } @Override public String getName() { return loggerContext.getName(); } private Configuration getConfig() { return loggerContext.getConfiguration(); } @Override public String getConfigLocationUri() { if (loggerContext.getConfigLocation() != null) { return String.valueOf(loggerContext.getConfigLocation()); } if (getConfigName() != null) { return String.valueOf(new File(getConfigName()).toURI()); } return Strings.EMPTY; } @Override public void setConfigLocationUri(final String configLocation) throws URISyntaxException, IOException { if (configLocation == null || configLocation.isEmpty()) { throw new IllegalArgumentException("Missing configuration location"); } LOGGER.debug("---------"); LOGGER.debug("Remote request to reconfigure using location " + configLocation); final File configFile = new File(configLocation); ConfigurationSource configSource = null; if (configFile.exists()) { LOGGER.debug("Opening config file {}", configFile.getAbsolutePath()); configSource = new ConfigurationSource(new FileInputStream(configFile), configFile); } else { final URL configURL = new URL(configLocation); LOGGER.debug("Opening config URL {}", configURL); configSource = new ConfigurationSource(configURL.openStream(), configURL); } final Configuration config = ConfigurationFactory.getInstance().getConfiguration(loggerContext, configSource); loggerContext.start(config); LOGGER.debug("Completed remote request to reconfigure."); } @Override public void propertyChange(final PropertyChangeEvent evt) { if (!LoggerContext.PROPERTY_CONFIG.equals(evt.getPropertyName())) { return; } final Notification notif = new Notification(NOTIF_TYPE_RECONFIGURED, getObjectName(), nextSeqNo(), now(), null); sendNotification(notif); } @Override public String getConfigText() throws IOException { return getConfigText(StandardCharsets.UTF_8.name()); } @Override public String getConfigText(final String charsetName) throws IOException { try { final ConfigurationSource source = loggerContext.getConfiguration().getConfigurationSource(); final ConfigurationSource copy = source.resetInputStream(); final Charset charset = Charset.forName(charsetName); return readContents(copy.getInputStream(), charset); } catch (final Exception ex) { final StringWriter sw = new StringWriter(BUFFER_SIZE); ex.printStackTrace(new PrintWriter(sw)); return sw.toString(); } } /** * Returns the contents of the specified input stream as a String. * @param in stream to read from * @param charset MUST not be null * @return stream contents * @throws IOException if a problem occurred reading from the stream. */ private String readContents(final InputStream in, final Charset charset) throws IOException { Reader reader = null; try { reader = new InputStreamReader(in, charset); final StringBuilder result = new StringBuilder(TEXT_BUFFER); final char[] buff = new char[PAGE]; int count = -1; while ((count = reader.read(buff)) >= 0) { result.append(buff, 0, count); } return result.toString(); } finally { Closer.closeSilently(in); Closer.closeSilently(reader); } } @Override public void setConfigText(final String configText, final String charsetName) { LOGGER.debug("---------"); LOGGER.debug("Remote request to reconfigure from config text."); try { final InputStream in = new ByteArrayInputStream(configText.getBytes(charsetName)); final ConfigurationSource source = new ConfigurationSource(in); final Configuration updated = ConfigurationFactory.getInstance().getConfiguration(loggerContext, source); loggerContext.start(updated); LOGGER.debug("Completed remote request to reconfigure from config text."); } catch (final Exception ex) { final String msg = "Could not reconfigure from config text"; LOGGER.error(msg, ex); throw new IllegalArgumentException(msg, ex); } } @Override public String getConfigName() { return getConfig().getName(); } @Override public String getConfigClassName() { return getConfig().getClass().getName(); } @Override public String getConfigFilter() { return String.valueOf(getConfig().getFilter()); } @Override public Map<String, String> getConfigProperties() { return getConfig().getProperties(); } /** * Returns the {@code ObjectName} of this mbean. * * @return the {@code ObjectName} * @see LoggerContextAdminMBean#PATTERN */ @Override public ObjectName getObjectName() { return objectName; } private long nextSeqNo() { return sequenceNo.getAndIncrement(); } private long now() { return System.currentTimeMillis(); } }