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.core.appender.routing;
018
019import java.util.Collections;
020import java.util.Map;
021import java.util.Objects;
022import java.util.concurrent.ConcurrentHashMap;
023import java.util.concurrent.ConcurrentMap;
024import java.util.concurrent.TimeUnit;
025
026import javax.script.Bindings;
027
028import org.apache.logging.log4j.core.Appender;
029import org.apache.logging.log4j.core.Core;
030import org.apache.logging.log4j.core.Filter;
031import org.apache.logging.log4j.core.LifeCycle2;
032import org.apache.logging.log4j.core.LogEvent;
033import org.apache.logging.log4j.core.appender.AbstractAppender;
034import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
035import org.apache.logging.log4j.core.config.AppenderControl;
036import org.apache.logging.log4j.core.config.Configuration;
037import org.apache.logging.log4j.core.config.Node;
038import org.apache.logging.log4j.core.config.plugins.Plugin;
039import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
040import org.apache.logging.log4j.core.config.plugins.PluginElement;
041import org.apache.logging.log4j.core.script.AbstractScript;
042import org.apache.logging.log4j.core.script.ScriptManager;
043import org.apache.logging.log4j.core.util.Booleans;
044
045/**
046 * This Appender "routes" between various Appenders, some of which can be references to
047 * Appenders defined earlier in the configuration while others can be dynamically created
048 * within this Appender as required. Routing is achieved by specifying a pattern on
049 * the Routing appender declaration. The pattern should contain one or more substitution patterns of
050 * the form "$${[key:]token}". The pattern will be resolved each time the Appender is called using
051 * the built in StrSubstitutor and the StrLookup plugin that matches the specified key.
052 */
053@Plugin(name = "Routing", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
054public final class RoutingAppender extends AbstractAppender {
055
056    public static final String STATIC_VARIABLES_KEY = "staticVariables";
057
058    public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
059            implements org.apache.logging.log4j.core.util.Builder<RoutingAppender> {
060
061        // Does not work unless the element is called "Script", I wanted "DefaultRounteScript"...
062        @PluginElement("Script")
063        private AbstractScript defaultRouteScript;
064
065        @PluginElement("Routes")
066        private Routes routes;
067
068        @PluginElement("RewritePolicy")
069        private RewritePolicy rewritePolicy;
070
071        @PluginElement("PurgePolicy")
072        private PurgePolicy purgePolicy;
073
074        @Override
075        public RoutingAppender build() {
076            final String name = getName();
077            if (name == null) {
078                LOGGER.error("No name defined for this RoutingAppender");
079                return null;
080            }
081            if (routes == null) {
082                LOGGER.error("No routes defined for RoutingAppender {}", name);
083                return null;
084            }
085            return new RoutingAppender(name, getFilter(), isIgnoreExceptions(), routes, rewritePolicy,
086                    getConfiguration(), purgePolicy, defaultRouteScript);
087        }
088
089        public Routes getRoutes() {
090            return routes;
091        }
092
093        public AbstractScript getDefaultRouteScript() {
094            return defaultRouteScript;
095        }
096
097        public RewritePolicy getRewritePolicy() {
098            return rewritePolicy;
099        }
100
101        public PurgePolicy getPurgePolicy() {
102            return purgePolicy;
103        }
104
105        public B withRoutes(@SuppressWarnings("hiding") final Routes routes) {
106            this.routes = routes;
107            return asBuilder();
108        }
109
110        public B withDefaultRouteScript(@SuppressWarnings("hiding") final AbstractScript defaultRouteScript) {
111            this.defaultRouteScript = defaultRouteScript;
112            return asBuilder();
113        }
114
115        public B withRewritePolicy(@SuppressWarnings("hiding") final RewritePolicy rewritePolicy) {
116            this.rewritePolicy = rewritePolicy;
117            return asBuilder();
118        }
119
120        public void withPurgePolicy(@SuppressWarnings("hiding") final PurgePolicy purgePolicy) {
121            this.purgePolicy = purgePolicy;
122        }
123
124    }
125
126    @PluginBuilderFactory
127    public static <B extends Builder<B>> B newBuilder() {
128        return new Builder<B>().asBuilder();
129    }
130
131    private static final String DEFAULT_KEY = "ROUTING_APPENDER_DEFAULT";
132
133    private final Routes routes;
134    private Route defaultRoute;
135    private final Configuration configuration;
136    private final ConcurrentMap<String, AppenderControl> appenders = new ConcurrentHashMap<>();
137    private final RewritePolicy rewritePolicy;
138    private final PurgePolicy purgePolicy;
139    private final AbstractScript defaultRouteScript;
140    private final ConcurrentMap<Object, Object> scriptStaticVariables = new ConcurrentHashMap<>();
141
142    private RoutingAppender(final String name, final Filter filter, final boolean ignoreExceptions, final Routes routes,
143            final RewritePolicy rewritePolicy, final Configuration configuration, final PurgePolicy purgePolicy,
144            final AbstractScript defaultRouteScript) {
145        super(name, filter, null, ignoreExceptions);
146        this.routes = routes;
147        this.configuration = configuration;
148        this.rewritePolicy = rewritePolicy;
149        this.purgePolicy = purgePolicy;
150        if (this.purgePolicy != null) {
151            this.purgePolicy.initialize(this);
152        }
153        this.defaultRouteScript = defaultRouteScript;
154        Route defRoute = null;
155        for (final Route route : routes.getRoutes()) {
156            if (route.getKey() == null) {
157                if (defRoute == null) {
158                    defRoute = route;
159                } else {
160                    error("Multiple default routes. Route " + route.toString() + " will be ignored");
161                }
162            }
163        }
164        defaultRoute = defRoute;
165    }
166
167    @Override
168    public void start() {
169        if (defaultRouteScript != null) {
170            if (configuration == null) {
171                error("No Configuration defined for RoutingAppender; required for Script element.");
172            } else {
173                final ScriptManager scriptManager = configuration.getScriptManager();
174                scriptManager.addScript(defaultRouteScript);
175                final Bindings bindings = scriptManager.createBindings(defaultRouteScript);
176                bindings.put(STATIC_VARIABLES_KEY, scriptStaticVariables);
177                final Object object = scriptManager.execute(defaultRouteScript.getName(), bindings);
178                final Route route = routes.getRoute(Objects.toString(object, null));
179                if (route != null) {
180                    defaultRoute = route;
181                }
182            }
183        }
184        // Register all the static routes.
185        for (final Route route : routes.getRoutes()) {
186            if (route.getAppenderRef() != null) {
187                final Appender appender = configuration.getAppender(route.getAppenderRef());
188                if (appender != null) {
189                    final String key = route == defaultRoute ? DEFAULT_KEY : route.getKey();
190                    appenders.put(key, new AppenderControl(appender, null, null));
191                } else {
192                    error("Appender " + route.getAppenderRef() + " cannot be located. Route ignored");
193                }
194            }
195        }
196        super.start();
197    }
198
199    @Override
200    public boolean stop(final long timeout, final TimeUnit timeUnit) {
201        setStopping();
202        super.stop(timeout, timeUnit, false);
203        final Map<String, Appender> map = configuration.getAppenders();
204        for (final Map.Entry<String, AppenderControl> entry : appenders.entrySet()) {
205            final Appender appender = entry.getValue().getAppender();
206            if (!map.containsKey(appender.getName())) {
207                if (appender instanceof LifeCycle2) {
208                    ((LifeCycle2) appender).stop(timeout, timeUnit);
209                } else {
210                    appender.stop();
211                }
212            }
213        }
214        setStopped();
215        return true;
216    }
217
218    @Override
219    public void append(LogEvent event) {
220        if (rewritePolicy != null) {
221            event = rewritePolicy.rewrite(event);
222        }
223        final String pattern = routes.getPattern(event, scriptStaticVariables);
224        final String key = pattern != null ? configuration.getStrSubstitutor().replace(event, pattern) : defaultRoute.getKey();
225        final AppenderControl control = getControl(key, event);
226        if (control != null) {
227            control.callAppender(event);
228        }
229
230        if (purgePolicy != null) {
231            purgePolicy.update(key, event);
232        }
233    }
234
235    private synchronized AppenderControl getControl(final String key, final LogEvent event) {
236        AppenderControl control = appenders.get(key);
237        if (control != null) {
238            return control;
239        }
240        Route route = null;
241        for (final Route r : routes.getRoutes()) {
242            if (r.getAppenderRef() == null && key.equals(r.getKey())) {
243                route = r;
244                break;
245            }
246        }
247        if (route == null) {
248            route = defaultRoute;
249            control = appenders.get(DEFAULT_KEY);
250            if (control != null) {
251                return control;
252            }
253        }
254        if (route != null) {
255            final Appender app = createAppender(route, event);
256            if (app == null) {
257                return null;
258            }
259            control = new AppenderControl(app, null, null);
260            appenders.put(key, control);
261        }
262
263        return control;
264    }
265
266    private Appender createAppender(final Route route, final LogEvent event) {
267        final Node routeNode = route.getNode();
268        for (final Node node : routeNode.getChildren()) {
269            if (node.getType().getElementName().equals(Appender.ELEMENT_TYPE)) {
270                final Node appNode = new Node(node);
271                configuration.createConfiguration(appNode, event);
272                if (appNode.getObject() instanceof Appender) {
273                    final Appender app = appNode.getObject();
274                    app.start();
275                    return app;
276                }
277                error("Unable to create Appender of type " + node.getName());
278                return null;
279            }
280        }
281        error("No Appender was configured for route " + route.getKey());
282        return null;
283    }
284
285    public Map<String, AppenderControl> getAppenders() {
286        return Collections.unmodifiableMap(appenders);
287    }
288
289    /**
290     * Deletes the specified appender.
291     *
292     * @param key The appender's key
293     */
294    public void deleteAppender(final String key) {
295        LOGGER.debug("Deleting route with " + key + " key ");
296        final AppenderControl control = appenders.remove(key);
297        if (null != control) {
298            LOGGER.debug("Stopping route with " + key + " key");
299            control.getAppender().stop();
300        } else {
301            LOGGER.debug("Route with " + key + " key already deleted");
302        }
303    }
304
305    /**
306     * Creates a RoutingAppender.
307     * @param name The name of the Appender.
308     * @param ignore If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise
309     *               they are propagated to the caller.
310     * @param routes The routing definitions.
311     * @param config The Configuration (automatically added by the Configuration).
312     * @param rewritePolicy A RewritePolicy, if any.
313     * @param filter A Filter to restrict events processed by the Appender or null.
314     * @return The RoutingAppender
315     * @deprecated Since 2.7; use {@link #newBuilder()}
316     */
317    @Deprecated
318    public static RoutingAppender createAppender(
319            final String name,
320            final String ignore,
321            final Routes routes,
322            final Configuration config,
323            final RewritePolicy rewritePolicy,
324            final PurgePolicy purgePolicy,
325            final Filter filter) {
326
327        final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
328        if (name == null) {
329            LOGGER.error("No name provided for RoutingAppender");
330            return null;
331        }
332        if (routes == null) {
333            LOGGER.error("No routes defined for RoutingAppender");
334            return null;
335        }
336        return new RoutingAppender(name, filter, ignoreExceptions, routes, rewritePolicy, config, purgePolicy, null);
337    }
338
339    public Route getDefaultRoute() {
340        return defaultRoute;
341    }
342
343    public AbstractScript getDefaultRouteScript() {
344        return defaultRouteScript;
345    }
346
347    public PurgePolicy getPurgePolicy() {
348        return purgePolicy;
349    }
350
351    public RewritePolicy getRewritePolicy() {
352        return rewritePolicy;
353    }
354
355    public Routes getRoutes() {
356        return routes;
357    }
358
359    public Configuration getConfiguration() {
360        return configuration;
361    }
362
363    public ConcurrentMap<Object, Object> getScriptStaticVariables() {
364        return scriptStaticVariables;
365    }
366}