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