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