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