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.logging.log4j.spring.boot;
018
019import java.io.File;
020import java.io.FileNotFoundException;
021import java.io.IOException;
022import java.io.UnsupportedEncodingException;
023import java.net.MalformedURLException;
024import java.net.URISyntaxException;
025import java.net.URL;
026import java.net.URLConnection;
027import java.net.URLDecoder;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.List;
031import java.util.Properties;
032import javax.net.ssl.HttpsURLConnection;
033
034import org.apache.logging.log4j.LogManager;
035import org.apache.logging.log4j.Logger;
036import org.apache.logging.log4j.core.LoggerContext;
037import org.apache.logging.log4j.core.config.AbstractConfiguration;
038import org.apache.logging.log4j.core.config.Configuration;
039import org.apache.logging.log4j.core.config.ConfigurationFactory;
040import org.apache.logging.log4j.core.config.ConfigurationSource;
041import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
042import org.apache.logging.log4j.core.net.ssl.LaxHostnameVerifier;
043import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
044import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory;
045import org.apache.logging.log4j.core.util.AuthorizationProvider;
046import org.apache.logging.log4j.core.util.FileUtils;
047import org.apache.logging.log4j.status.StatusLogger;
048import org.apache.logging.log4j.util.PropertiesUtil;
049import org.apache.logging.log4j.util.Strings;
050import org.springframework.boot.logging.LogFile;
051import org.springframework.boot.logging.LoggingInitializationContext;
052import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem;
053import org.springframework.util.Assert;
054import org.springframework.util.ClassUtils;
055import org.springframework.util.ResourceUtils;
056
057/**
058 * Override Spring's implementation of the Log4j 2 Logging System to properly support Spring Cloud Config.
059 */
060public class Log4j2CloudConfigLoggingSystem extends Log4J2LoggingSystem {
061    private static final String HTTPS = "https";
062    public static final String ENVIRONMENT_KEY = "SpringEnvironment";
063    private static final String OVERRIDE_PARAM = "override";
064    private static Logger LOGGER = StatusLogger.getLogger();
065
066    public Log4j2CloudConfigLoggingSystem(ClassLoader loader) {
067        super(loader);
068    }
069
070    /**
071     * Set the environment into the ExternalContext field so that it can be obtained by SpringLookup when it
072     * is constructed. Spring will replace the ExternalContext field with a String once initialization is
073     * complete.
074     * @param initializationContext The initialization context.
075     * @param configLocation The configuration location.
076     * @param logFile the log file.
077     */
078    @Override
079    public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
080        getLoggerContext().putObjectIfAbsent(ENVIRONMENT_KEY, initializationContext.getEnvironment());
081        super.initialize(initializationContext, configLocation, logFile);
082    }
083
084    @Override
085    protected String[] getStandardConfigLocations() {
086        String[] locations = super.getStandardConfigLocations();
087        PropertiesUtil props = new PropertiesUtil(new Properties());
088        String location = props.getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
089        if (location != null) {
090            List<String> list = new ArrayList<>(Arrays.asList(super.getStandardConfigLocations()));
091            list.add(location);
092            locations = list.toArray(Strings.EMPTY_ARRAY);
093        }
094        return locations;
095    }
096
097    @Override
098    protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
099        if (logFile != null) {
100            this.loadConfiguration(this.getBootPackagedConfigFile("log4j2-file.xml"), logFile);
101        } else {
102            this.loadConfiguration(this.getBootPackagedConfigFile("log4j2.xml"), logFile);
103        }
104    }
105
106    private String getBootPackagedConfigFile(String fileName) {
107        String defaultPath = ClassUtils.getPackageName(Log4J2LoggingSystem.class);
108        defaultPath = defaultPath.replace('.', '/');
109        defaultPath = defaultPath + "/" + fileName;
110        defaultPath = "classpath:" + defaultPath;
111        return defaultPath;
112    }
113
114    @Override
115    protected void loadConfiguration(String location, LogFile logFile) {
116        Assert.notNull(location, "Location must not be null");
117        try {
118            LoggerContext ctx = getLoggerContext();
119            String[] locations = parseConfigLocations(location);
120            if (locations.length == 1) {
121                final URL url = ResourceUtils.getURL(location);
122                final ConfigurationSource source = getConfigurationSource(url);
123                if (source != null) {
124                    ctx.start(ConfigurationFactory.getInstance().getConfiguration(ctx, source));
125                }
126            } else {
127                final List<AbstractConfiguration> configs = new ArrayList<>();
128                for (final String sourceLocation : locations) {
129                    final ConfigurationSource source = getConfigurationSource(ResourceUtils.getURL(sourceLocation));
130                    if (source != null) {
131                        final Configuration config = ConfigurationFactory.getInstance().getConfiguration(ctx, source);
132                        if (config instanceof AbstractConfiguration) {
133                            configs.add((AbstractConfiguration) config);
134                        } else {
135                            LOGGER.warn("Configuration at {} cannot be combined in a CompositeConfiguration", sourceLocation);
136                            return;
137                        }
138                    }
139                }
140                if (configs.size() > 1) {
141                    ctx.start(new CompositeConfiguration(configs));
142                } else {
143                    ctx.start(configs.get(0));
144                }
145            }
146        }
147        catch (Exception ex) {
148            throw new IllegalStateException(
149                "Could not initialize Log4J2 logging from " + location, ex);
150        }
151    }
152
153    @Override
154    public void cleanUp() {
155        getLoggerContext().removeObject(ENVIRONMENT_KEY);
156        super.cleanUp();
157    }
158
159    private String[] parseConfigLocations(String configLocations) {
160        final String[] uris = configLocations.split("\\?");
161        final List<String> locations = new ArrayList<>();
162        if (uris.length > 1) {
163            locations.add(uris[0]);
164            try {
165                final URL url = new URL(configLocations);
166                final String[] pairs = url.getQuery().split("&");
167                for (String pair : pairs) {
168                    final int idx = pair.indexOf("=");
169                    try {
170                        final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair;
171                        if (key.equalsIgnoreCase(OVERRIDE_PARAM)) {
172                            locations.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
173                        }
174                    } catch (UnsupportedEncodingException ex) {
175                        LOGGER.warn("Bad data in configuration string: {}", pair);
176                    }
177                }
178                return locations.toArray(Strings.EMPTY_ARRAY);
179            } catch (MalformedURLException ex) {
180                LOGGER.warn("Unable to parse configuration URL {}", configLocations);
181            }
182        }
183        return new String[] {uris[0]};
184    }
185
186    private ConfigurationSource getConfigurationSource(URL url) throws IOException, URISyntaxException {
187        URLConnection urlConnection = url.openConnection();
188        AuthorizationProvider provider = ConfigurationFactory.authorizationProvider(PropertiesUtil.getProperties());
189        provider.addAuthorization(urlConnection);
190        if (url.getProtocol().equals(HTTPS)) {
191            SslConfiguration sslConfiguration = SslConfigurationFactory.getSslConfiguration();
192            if (sslConfiguration != null) {
193                ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslConfiguration.getSslSocketFactory());
194                if (!sslConfiguration.isVerifyHostName()) {
195                    ((HttpsURLConnection) urlConnection).setHostnameVerifier(LaxHostnameVerifier.INSTANCE);
196                }
197            }
198        }
199        File file = FileUtils.fileFromUri(url.toURI());
200        try {
201            if (file != null) {
202                return new ConfigurationSource(urlConnection.getInputStream(), FileUtils.fileFromUri(url.toURI()));
203            }
204            return new ConfigurationSource(urlConnection.getInputStream(), url, urlConnection.getLastModified());
205        } catch (FileNotFoundException ex) {
206            LOGGER.info("Unable to locate file {}, ignoring.", url.toString());
207            return null;
208        }
209    }
210
211    private LoggerContext getLoggerContext() {
212        return (LoggerContext) LogManager.getContext(false);
213    }
214}