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;
025import java.util.concurrent.atomic.AtomicInteger;
026
027import javax.script.Bindings;
028
029import org.apache.logging.log4j.core.Appender;
030import org.apache.logging.log4j.core.Core;
031import org.apache.logging.log4j.core.Filter;
032import org.apache.logging.log4j.core.LifeCycle2;
033import org.apache.logging.log4j.core.LogEvent;
034import org.apache.logging.log4j.core.appender.AbstractAppender;
035import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
036import org.apache.logging.log4j.core.config.AppenderControl;
037import org.apache.logging.log4j.core.config.Configuration;
038import org.apache.logging.log4j.core.config.Node;
039import org.apache.logging.log4j.core.config.Property;
040import org.apache.logging.log4j.core.config.plugins.Plugin;
041import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
042import org.apache.logging.log4j.core.config.plugins.PluginElement;
043import org.apache.logging.log4j.core.script.AbstractScript;
044import org.apache.logging.log4j.core.script.ScriptManager;
045import org.apache.logging.log4j.core.util.Booleans;
046
047/**
048 * This Appender "routes" between various Appenders, some of which can be references to
049 * Appenders defined earlier in the configuration while others can be dynamically created
050 * within this Appender as required. Routing is achieved by specifying a pattern on
051 * the Routing appender declaration. The pattern should contain one or more substitution patterns of
052 * the form "$${[key:]token}". The pattern will be resolved each time the Appender is called using
053 * the built in StrSubstitutor and the StrLookup plugin that matches the specified key.
054 */
055@Plugin(name = "Routing", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
056public final class RoutingAppender extends AbstractAppender {
057
058    public static final String STATIC_VARIABLES_KEY = "staticVariables";
059
060    public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
061            implements org.apache.logging.log4j.core.util.Builder<RoutingAppender> {
062
063        // Does not work unless the element is called "Script", I wanted "DefaultRounteScript"...
064        @PluginElement("Script")
065        private AbstractScript defaultRouteScript;
066
067        @PluginElement("Routes")
068        private Routes routes;
069
070        @PluginElement("RewritePolicy")
071        private RewritePolicy rewritePolicy;
072
073        @PluginElement("PurgePolicy")
074        private PurgePolicy purgePolicy;
075
076        @Override
077        public RoutingAppender build() {
078            final String name = getName();
079            if (name == null) {
080                LOGGER.error("No name defined for this RoutingAppender");
081                return null;
082            }
083            if (routes == null) {
084                LOGGER.error("No routes defined for RoutingAppender {}", name);
085                return null;
086            }
087            return new RoutingAppender(name, getFilter(), isIgnoreExceptions(), routes, rewritePolicy,
088                    getConfiguration(), purgePolicy, defaultRouteScript, getPropertyArray());
089        }
090
091        public Routes getRoutes() {
092            return routes;
093        }
094
095        public AbstractScript getDefaultRouteScript() {
096            return defaultRouteScript;
097        }
098
099        public RewritePolicy getRewritePolicy() {
100            return rewritePolicy;
101        }
102
103        public PurgePolicy getPurgePolicy() {
104            return purgePolicy;
105        }
106
107        public B withRoutes(@SuppressWarnings("hiding") final Routes routes) {
108            this.routes = routes;
109            return asBuilder();
110        }
111
112        public B withDefaultRouteScript(@SuppressWarnings("hiding") final AbstractScript defaultRouteScript) {
113            this.defaultRouteScript = defaultRouteScript;
114            return asBuilder();
115        }
116
117        public B withRewritePolicy(@SuppressWarnings("hiding") final RewritePolicy rewritePolicy) {
118            this.rewritePolicy = rewritePolicy;
119            return asBuilder();
120        }
121
122        public void withPurgePolicy(@SuppressWarnings("hiding") final PurgePolicy purgePolicy) {
123            this.purgePolicy = purgePolicy;
124        }
125
126    }
127
128    @PluginBuilderFactory
129    public static <B extends Builder<B>> B newBuilder() {
130        return new Builder<B>().asBuilder();
131    }
132
133    private static final String DEFAULT_KEY = "ROUTING_APPENDER_DEFAULT";
134
135    private final Routes routes;
136    private Route defaultRoute;
137    private final Configuration configuration;
138    private final ConcurrentMap<String, CreatedRouteAppenderControl> createdAppenders = new ConcurrentHashMap<>();
139    private final Map<String, AppenderControl> createdAppendersUnmodifiableView  = Collections.unmodifiableMap(
140            (Map<String, AppenderControl>) (Map<String, ?>) createdAppenders);
141    private final ConcurrentMap<String, RouteAppenderControl> referencedAppenders = new ConcurrentHashMap<>();
142    private final RewritePolicy rewritePolicy;
143    private final PurgePolicy purgePolicy;
144    private final AbstractScript defaultRouteScript;
145    private final ConcurrentMap<Object, Object> scriptStaticVariables = new ConcurrentHashMap<>();
146
147    private RoutingAppender(final String name, final Filter filter, final boolean ignoreExceptions, final Routes routes,
148            final RewritePolicy rewritePolicy, final Configuration configuration, final PurgePolicy purgePolicy,
149            final AbstractScript defaultRouteScript, final Property[] properties) {
150        super(name, filter, null, ignoreExceptions, properties);
151        this.routes = routes;
152        this.configuration = configuration;
153        this.rewritePolicy = rewritePolicy;
154        this.purgePolicy = purgePolicy;
155        if (this.purgePolicy != null) {
156            this.purgePolicy.initialize(this);
157        }
158        this.defaultRouteScript = defaultRouteScript;
159        Route defRoute = null;
160        for (final Route route : routes.getRoutes()) {
161            if (route.getKey() == null) {
162                if (defRoute == null) {
163                    defRoute = route;
164                } else {
165                    error("Multiple default routes. Route " + route.toString() + " will be ignored");
166                }
167            }
168        }
169        defaultRoute = defRoute;
170    }
171
172    @Override
173    public void start() {
174        if (defaultRouteScript != null) {
175            if (configuration == null) {
176                error("No Configuration defined for RoutingAppender; required for Script element.");
177            } else {
178                final ScriptManager scriptManager = configuration.getScriptManager();
179                scriptManager.addScript(defaultRouteScript);
180                final Bindings bindings = scriptManager.createBindings(defaultRouteScript);
181                bindings.put(STATIC_VARIABLES_KEY, scriptStaticVariables);
182                final Object object = scriptManager.execute(defaultRouteScript.getName(), bindings);
183                final Route route = routes.getRoute(Objects.toString(object, null));
184                if (route != null) {
185                    defaultRoute = route;
186                }
187            }
188        }
189        // Register all the static routes.
190        for (final Route route : routes.getRoutes()) {
191            if (route.getAppenderRef() != null) {
192                final Appender appender = configuration.getAppender(route.getAppenderRef());
193                if (appender != null) {
194                    final String key = route == defaultRoute ? DEFAULT_KEY : route.getKey();
195                    referencedAppenders.put(key, new ReferencedRouteAppenderControl(appender));
196                } else {
197                    error("Appender " + route.getAppenderRef() + " cannot be located. Route ignored");
198                }
199            }
200        }
201        super.start();
202    }
203
204    @Override
205    public boolean stop(final long timeout, final TimeUnit timeUnit) {
206        setStopping();
207        super.stop(timeout, timeUnit, false);
208        // Only stop appenders that were created by this RoutingAppender
209        for (final Map.Entry<String, CreatedRouteAppenderControl> entry : createdAppenders.entrySet()) {
210            final Appender appender = entry.getValue().getAppender();
211            if (appender instanceof LifeCycle2) {
212                ((LifeCycle2) appender).stop(timeout, timeUnit);
213            } else {
214                appender.stop();
215            }
216        }
217        setStopped();
218        return true;
219    }
220
221    @Override
222    public void append(LogEvent event) {
223        if (rewritePolicy != null) {
224            event = rewritePolicy.rewrite(event);
225        }
226        final String pattern = routes.getPattern(event, scriptStaticVariables);
227        final String key = pattern != null ? configuration.getStrSubstitutor().replace(event, pattern) : defaultRoute.getKey();
228        final RouteAppenderControl control = getControl(key, event);
229        if (control != null) {
230            try {
231                control.callAppender(event);
232            } finally {
233                control.release();
234            }
235        }
236        updatePurgePolicy(key, event);
237    }
238
239    private void updatePurgePolicy(final String key, final LogEvent event) {
240        if (purgePolicy != null
241                // LOG4J2-2631: PurgePolicy implementations do not need to be aware of appenders that
242                // were not created by this RoutingAppender.
243                && !referencedAppenders.containsKey(key)) {
244            purgePolicy.update(key, event);
245        }
246    }
247
248    private synchronized RouteAppenderControl getControl(final String key, final LogEvent event) {
249        RouteAppenderControl control = getAppender(key);
250        if (control != null) {
251            control.checkout();
252            return control;
253        }
254        Route route = null;
255        for (final Route r : routes.getRoutes()) {
256            if (r.getAppenderRef() == null && key.equals(r.getKey())) {
257                route = r;
258                break;
259            }
260        }
261        if (route == null) {
262            route = defaultRoute;
263            control = getAppender(DEFAULT_KEY);
264            if (control != null) {
265                control.checkout();
266                return control;
267            }
268        }
269        if (route != null) {
270            final Appender app = createAppender(route, event);
271            if (app == null) {
272                return null;
273            }
274            CreatedRouteAppenderControl created = new CreatedRouteAppenderControl(app);
275            control = created;
276            createdAppenders.put(key, created);
277        }
278
279        if (control != null) {
280            control.checkout();
281        }
282        return control;
283    }
284
285    private RouteAppenderControl getAppender(final String key) {
286        final RouteAppenderControl result = referencedAppenders.get(key);
287        if (result == null) {
288            return createdAppenders.get(key);
289        }
290        return result;
291    }
292
293    private Appender createAppender(final Route route, final LogEvent event) {
294        final Node routeNode = route.getNode();
295        for (final Node node : routeNode.getChildren()) {
296            if (node.getType().getElementName().equals(Appender.ELEMENT_TYPE)) {
297                final Node appNode = new Node(node);
298                configuration.createConfiguration(appNode, event);
299                if (appNode.getObject() instanceof Appender) {
300                    final Appender app = appNode.getObject();
301                    app.start();
302                    return app;
303                }
304                error("Unable to create Appender of type " + node.getName());
305                return null;
306            }
307        }
308        error("No Appender was configured for route " + route.getKey());
309        return null;
310    }
311
312    /**
313     * Returns an unmodifiable view of the appenders created by this {@link RoutingAppender}.
314     * Note that this map does not contain appenders that are routed by reference.
315     */
316    public Map<String, AppenderControl> getAppenders() {
317        return createdAppendersUnmodifiableView;
318    }
319
320    /**
321     * Deletes the specified appender.
322     *
323     * @param key The appender's key
324     */
325    public void deleteAppender(final String key) {
326        LOGGER.debug("Deleting route with {} key ", key);
327        // LOG4J2-2631: Only appenders created by this RoutingAppender are eligible for deletion.
328        final CreatedRouteAppenderControl control = createdAppenders.remove(key);
329        if (null != control) {
330            LOGGER.debug("Stopping route with {} key", key);
331            // Synchronize with getControl to avoid triggering stopAppender before RouteAppenderControl.checkout
332            // can be invoked.
333            synchronized (this) {
334                control.pendingDeletion = true;
335            }
336            // Don't attempt to stop the appender in a synchronized block, since it may block flushing events
337            // to disk.
338            control.tryStopAppender();
339        } else {
340            if (referencedAppenders.containsKey(key)) {
341                LOGGER.debug("Route {} using an appender reference may not be removed because " +
342                        "the appender may be used outside of the RoutingAppender", key);
343            } else {
344                LOGGER.debug("Route with {} key already deleted", key);
345            }
346        }
347    }
348
349    /**
350     * Creates a RoutingAppender.
351     * @param name The name of the Appender.
352     * @param ignore If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise
353     *               they are propagated to the caller.
354     * @param routes The routing definitions.
355     * @param config The Configuration (automatically added by the Configuration).
356     * @param rewritePolicy A RewritePolicy, if any.
357     * @param filter A Filter to restrict events processed by the Appender or null.
358     * @return The RoutingAppender
359     * @deprecated Since 2.7; use {@link #newBuilder()}
360     */
361    @Deprecated
362    public static RoutingAppender createAppender(
363            final String name,
364            final String ignore,
365            final Routes routes,
366            final Configuration config,
367            final RewritePolicy rewritePolicy,
368            final PurgePolicy purgePolicy,
369            final Filter filter) {
370
371        final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
372        if (name == null) {
373            LOGGER.error("No name provided for RoutingAppender");
374            return null;
375        }
376        if (routes == null) {
377            LOGGER.error("No routes defined for RoutingAppender");
378            return null;
379        }
380        return new RoutingAppender(name, filter, ignoreExceptions, routes, rewritePolicy, config, purgePolicy, null, null);
381    }
382
383    public Route getDefaultRoute() {
384        return defaultRoute;
385    }
386
387    public AbstractScript getDefaultRouteScript() {
388        return defaultRouteScript;
389    }
390
391    public PurgePolicy getPurgePolicy() {
392        return purgePolicy;
393    }
394
395    public RewritePolicy getRewritePolicy() {
396        return rewritePolicy;
397    }
398
399    public Routes getRoutes() {
400        return routes;
401    }
402
403    public Configuration getConfiguration() {
404        return configuration;
405    }
406
407    public ConcurrentMap<Object, Object> getScriptStaticVariables() {
408        return scriptStaticVariables;
409    }
410
411    /**
412     * LOG4J2-2629: PurgePolicy implementations can invoke {@link #deleteAppender(String)} after we have looked up
413     * an instance of a target appender but before events are appended, which could result in events not being
414     * recorded to any appender.
415     * This extension of {@link AppenderControl} allows to to mark usage of an appender, allowing deferral of
416     * {@link Appender#stop()} until events have successfully been recorded.
417     * Alternative approaches considered:
418     * - More aggressive synchronization: Appenders may do expensive I/O that shouldn't block routing.
419     * - Move the 'updatePurgePolicy' invocation before appenders are called: Unfortunately this approach doesn't work
420     *   if we consider an ImmediatePurgePolicy (or IdlePurgePolicy with a very small timeout) because it may attempt
421     *   to remove an appender that doesn't exist yet. It's counterintuitive to get an event that a route has been
422     *   used at a point when we expect the route doesn't exist in {@link #getAppenders()}.
423     */
424    private static abstract class RouteAppenderControl extends AppenderControl {
425
426        RouteAppenderControl(Appender appender) {
427            super(appender, null, null);
428        }
429
430        abstract void checkout();
431
432        abstract void release();
433    }
434
435    private static final class CreatedRouteAppenderControl extends RouteAppenderControl {
436
437        private volatile boolean pendingDeletion = false;
438        private final AtomicInteger depth = new AtomicInteger();
439
440        CreatedRouteAppenderControl(Appender appender) {
441            super(appender);
442        }
443
444        @Override
445        void checkout() {
446            if (pendingDeletion) {
447                LOGGER.warn("CreatedRouteAppenderControl.checkout invoked on a " +
448                        "RouteAppenderControl that is pending deletion");
449            }
450            depth.incrementAndGet();
451        }
452
453        @Override
454        void release() {
455            depth.decrementAndGet();
456            tryStopAppender();
457        }
458
459        void tryStopAppender() {
460            if (pendingDeletion
461                    // Only attempt to stop the appender if we can CaS the depth away from zero, otherwise either
462                    // 1. Another invocation of tryStopAppender has succeeded, or
463                    // 2. Events are being appended, and will trigger stop when they complete
464                    && depth.compareAndSet(0, -100_000)) {
465                Appender appender = getAppender();
466                LOGGER.debug("Stopping appender {}", appender);
467                appender.stop();
468            }
469        }
470    }
471
472    private static final class ReferencedRouteAppenderControl extends RouteAppenderControl {
473
474        ReferencedRouteAppenderControl(Appender appender) {
475            super(appender);
476        }
477
478        @Override
479        void checkout() {
480            // nop
481        }
482
483        @Override
484        void release() {
485            // nop
486        }
487    }
488}