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) :
228                defaultRoute.getKey() != null ? defaultRoute.getKey() : DEFAULT_KEY;
229        final RouteAppenderControl control = getControl(key, event);
230        if (control != null) {
231            try {
232                control.callAppender(event);
233            } finally {
234                control.release();
235            }
236        }
237        updatePurgePolicy(key, event);
238    }
239
240    private void updatePurgePolicy(final String key, final LogEvent event) {
241        if (purgePolicy != null
242                // LOG4J2-2631: PurgePolicy implementations do not need to be aware of appenders that
243                // were not created by this RoutingAppender.
244                && !referencedAppenders.containsKey(key)) {
245            purgePolicy.update(key, event);
246        }
247    }
248
249    private synchronized RouteAppenderControl getControl(final String key, final LogEvent event) {
250        RouteAppenderControl control = getAppender(key);
251        if (control != null) {
252            control.checkout();
253            return control;
254        }
255        Route route = null;
256        for (final Route r : routes.getRoutes()) {
257            if (r.getAppenderRef() == null && key.equals(r.getKey())) {
258                route = r;
259                break;
260            }
261        }
262        if (route == null) {
263            route = defaultRoute;
264            control = getAppender(DEFAULT_KEY);
265            if (control != null) {
266                control.checkout();
267                return control;
268            }
269        }
270        if (route != null) {
271            final Appender app = createAppender(route, event);
272            if (app == null) {
273                return null;
274            }
275            CreatedRouteAppenderControl created = new CreatedRouteAppenderControl(app);
276            control = created;
277            createdAppenders.put(key, created);
278        }
279
280        if (control != null) {
281            control.checkout();
282        }
283        return control;
284    }
285
286    private RouteAppenderControl getAppender(final String key) {
287        final RouteAppenderControl result = referencedAppenders.get(key);
288        if (result == null) {
289            return createdAppenders.get(key);
290        }
291        return result;
292    }
293
294    private Appender createAppender(final Route route, final LogEvent event) {
295        final Node routeNode = route.getNode();
296        for (final Node node : routeNode.getChildren()) {
297            if (node.getType().getElementName().equals(Appender.ELEMENT_TYPE)) {
298                final Node appNode = new Node(node);
299                configuration.createConfiguration(appNode, event);
300                if (appNode.getObject() instanceof Appender) {
301                    final Appender app = appNode.getObject();
302                    app.start();
303                    return app;
304                }
305                error("Unable to create Appender of type " + node.getName());
306                return null;
307            }
308        }
309        error("No Appender was configured for route " + route.getKey());
310        return null;
311    }
312
313    /**
314     * Returns an unmodifiable view of the appenders created by this {@link RoutingAppender}.
315     * Note that this map does not contain appenders that are routed by reference.
316     */
317    public Map<String, AppenderControl> getAppenders() {
318        return createdAppendersUnmodifiableView;
319    }
320
321    /**
322     * Deletes the specified appender.
323     *
324     * @param key The appender's key
325     */
326    public void deleteAppender(final String key) {
327        LOGGER.debug("Deleting route with {} key ", key);
328        // LOG4J2-2631: Only appenders created by this RoutingAppender are eligible for deletion.
329        final CreatedRouteAppenderControl control = createdAppenders.remove(key);
330        if (null != control) {
331            LOGGER.debug("Stopping route with {} key", key);
332            // Synchronize with getControl to avoid triggering stopAppender before RouteAppenderControl.checkout
333            // can be invoked.
334            synchronized (this) {
335                control.pendingDeletion = true;
336            }
337            // Don't attempt to stop the appender in a synchronized block, since it may block flushing events
338            // to disk.
339            control.tryStopAppender();
340        } else 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     * Creates a RoutingAppender.
350     * @param name The name of the Appender.
351     * @param ignore If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise
352     *               they are propagated to the caller.
353     * @param routes The routing definitions.
354     * @param config The Configuration (automatically added by the Configuration).
355     * @param rewritePolicy A RewritePolicy, if any.
356     * @param filter A Filter to restrict events processed by the Appender or null.
357     * @return The RoutingAppender
358     * @deprecated Since 2.7; use {@link #newBuilder()}
359     */
360    @Deprecated
361    public static RoutingAppender createAppender(
362            final String name,
363            final String ignore,
364            final Routes routes,
365            final Configuration config,
366            final RewritePolicy rewritePolicy,
367            final PurgePolicy purgePolicy,
368            final Filter filter) {
369
370        final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
371        if (name == null) {
372            LOGGER.error("No name provided for RoutingAppender");
373            return null;
374        }
375        if (routes == null) {
376            LOGGER.error("No routes defined for RoutingAppender");
377            return null;
378        }
379        return new RoutingAppender(name, filter, ignoreExceptions, routes, rewritePolicy, config, purgePolicy, null, null);
380    }
381
382    public Route getDefaultRoute() {
383        return defaultRoute;
384    }
385
386    public AbstractScript getDefaultRouteScript() {
387        return defaultRouteScript;
388    }
389
390    public PurgePolicy getPurgePolicy() {
391        return purgePolicy;
392    }
393
394    public RewritePolicy getRewritePolicy() {
395        return rewritePolicy;
396    }
397
398    public Routes getRoutes() {
399        return routes;
400    }
401
402    public Configuration getConfiguration() {
403        return configuration;
404    }
405
406    public ConcurrentMap<Object, Object> getScriptStaticVariables() {
407        return scriptStaticVariables;
408    }
409
410    /**
411     * LOG4J2-2629: PurgePolicy implementations can invoke {@link #deleteAppender(String)} after we have looked up
412     * an instance of a target appender but before events are appended, which could result in events not being
413     * recorded to any appender.
414     * This extension of {@link AppenderControl} allows to to mark usage of an appender, allowing deferral of
415     * {@link Appender#stop()} until events have successfully been recorded.
416     * Alternative approaches considered:
417     * - More aggressive synchronization: Appenders may do expensive I/O that shouldn't block routing.
418     * - Move the 'updatePurgePolicy' invocation before appenders are called: Unfortunately this approach doesn't work
419     *   if we consider an ImmediatePurgePolicy (or IdlePurgePolicy with a very small timeout) because it may attempt
420     *   to remove an appender that doesn't exist yet. It's counterintuitive to get an event that a route has been
421     *   used at a point when we expect the route doesn't exist in {@link #getAppenders()}.
422     */
423    private static abstract class RouteAppenderControl extends AppenderControl {
424
425        RouteAppenderControl(Appender appender) {
426            super(appender, null, null);
427        }
428
429        abstract void checkout();
430
431        abstract void release();
432    }
433
434    private static final class CreatedRouteAppenderControl extends RouteAppenderControl {
435
436        private volatile boolean pendingDeletion;
437        private final AtomicInteger depth = new AtomicInteger();
438
439        CreatedRouteAppenderControl(Appender appender) {
440            super(appender);
441        }
442
443        @Override
444        void checkout() {
445            if (pendingDeletion) {
446                LOGGER.warn("CreatedRouteAppenderControl.checkout invoked on a " +
447                        "RouteAppenderControl that is pending deletion");
448            }
449            depth.incrementAndGet();
450        }
451
452        @Override
453        void release() {
454            depth.decrementAndGet();
455            tryStopAppender();
456        }
457
458        void tryStopAppender() {
459            if (pendingDeletion
460                    // Only attempt to stop the appender if we can CaS the depth away from zero, otherwise either
461                    // 1. Another invocation of tryStopAppender has succeeded, or
462                    // 2. Events are being appended, and will trigger stop when they complete
463                    && depth.compareAndSet(0, -100_000)) {
464                Appender appender = getAppender();
465                LOGGER.debug("Stopping appender {}", appender);
466                appender.stop();
467            }
468        }
469    }
470
471    private static final class ReferencedRouteAppenderControl extends RouteAppenderControl {
472
473        ReferencedRouteAppenderControl(Appender appender) {
474            super(appender);
475        }
476
477        @Override
478        void checkout() {
479            // nop
480        }
481
482        @Override
483        void release() {
484            // nop
485        }
486    }
487}