View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.appender.routing;
18  
19  import java.util.Collections;
20  import java.util.Map;
21  import java.util.Objects;
22  import java.util.concurrent.ConcurrentHashMap;
23  import java.util.concurrent.ConcurrentMap;
24  import java.util.concurrent.TimeUnit;
25  import java.util.concurrent.atomic.AtomicInteger;
26  
27  import javax.script.Bindings;
28  
29  import org.apache.logging.log4j.core.Appender;
30  import org.apache.logging.log4j.core.Core;
31  import org.apache.logging.log4j.core.Filter;
32  import org.apache.logging.log4j.core.LifeCycle2;
33  import org.apache.logging.log4j.core.LogEvent;
34  import org.apache.logging.log4j.core.appender.AbstractAppender;
35  import org.apache.logging.log4j.core.appender.rewrite.RewritePolicy;
36  import org.apache.logging.log4j.core.config.AppenderControl;
37  import org.apache.logging.log4j.core.config.Configuration;
38  import org.apache.logging.log4j.core.config.Node;
39  import org.apache.logging.log4j.core.config.Property;
40  import org.apache.logging.log4j.core.config.plugins.Plugin;
41  import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
42  import org.apache.logging.log4j.core.config.plugins.PluginElement;
43  import org.apache.logging.log4j.core.script.AbstractScript;
44  import org.apache.logging.log4j.core.script.ScriptManager;
45  import org.apache.logging.log4j.core.util.Booleans;
46  
47  /**
48   * This Appender "routes" between various Appenders, some of which can be references to
49   * Appenders defined earlier in the configuration while others can be dynamically created
50   * within this Appender as required. Routing is achieved by specifying a pattern on
51   * the Routing appender declaration. The pattern should contain one or more substitution patterns of
52   * the form "$${[key:]token}". The pattern will be resolved each time the Appender is called using
53   * the built in StrSubstitutor and the StrLookup plugin that matches the specified key.
54   */
55  @Plugin(name = "Routing", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
56  public final class RoutingAppender extends AbstractAppender {
57  
58      public static final String STATIC_VARIABLES_KEY = "staticVariables";
59  
60      public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
61              implements org.apache.logging.log4j.core.util.Builder<RoutingAppender> {
62  
63          // Does not work unless the element is called "Script", I wanted "DefaultRounteScript"...
64          @PluginElement("Script")
65          private AbstractScript defaultRouteScript;
66  
67          @PluginElement("Routes")
68          private Routes routes;
69  
70          @PluginElement("RewritePolicy")
71          private RewritePolicy rewritePolicy;
72  
73          @PluginElement("PurgePolicy")
74          private PurgePolicy purgePolicy;
75  
76          @Override
77          public RoutingAppender build() {
78              final String name = getName();
79              if (name == null) {
80                  LOGGER.error("No name defined for this RoutingAppender");
81                  return null;
82              }
83              if (routes == null) {
84                  LOGGER.error("No routes defined for RoutingAppender {}", name);
85                  return null;
86              }
87              return new RoutingAppender(name, getFilter(), isIgnoreExceptions(), routes, rewritePolicy,
88                      getConfiguration(), purgePolicy, defaultRouteScript, getPropertyArray());
89          }
90  
91          public Routes getRoutes() {
92              return routes;
93          }
94  
95          public AbstractScript getDefaultRouteScript() {
96              return defaultRouteScript;
97          }
98  
99          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 }