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) :
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 {
341             if (referencedAppenders.containsKey(key)) {
342                 LOGGER.debug("Route {} using an appender reference may not be removed because " +
343                         "the appender may be used outside of the RoutingAppender", key);
344             } else {
345                 LOGGER.debug("Route with {} key already deleted", key);
346             }
347         }
348     }
349 
350     /**
351      * Creates a RoutingAppender.
352      * @param name The name of the Appender.
353      * @param ignore If {@code "true"} (default) exceptions encountered when appending events are logged; otherwise
354      *               they are propagated to the caller.
355      * @param routes The routing definitions.
356      * @param config The Configuration (automatically added by the Configuration).
357      * @param rewritePolicy A RewritePolicy, if any.
358      * @param filter A Filter to restrict events processed by the Appender or null.
359      * @return The RoutingAppender
360      * @deprecated Since 2.7; use {@link #newBuilder()}
361      */
362     @Deprecated
363     public static RoutingAppender createAppender(
364             final String name,
365             final String ignore,
366             final Routes routes,
367             final Configuration config,
368             final RewritePolicy rewritePolicy,
369             final PurgePolicy purgePolicy,
370             final Filter filter) {
371 
372         final boolean ignoreExceptions = Booleans.parseBoolean(ignore, true);
373         if (name == null) {
374             LOGGER.error("No name provided for RoutingAppender");
375             return null;
376         }
377         if (routes == null) {
378             LOGGER.error("No routes defined for RoutingAppender");
379             return null;
380         }
381         return new RoutingAppender(name, filter, ignoreExceptions, routes, rewritePolicy, config, purgePolicy, null, null);
382     }
383 
384     public Route getDefaultRoute() {
385         return defaultRoute;
386     }
387 
388     public AbstractScript getDefaultRouteScript() {
389         return defaultRouteScript;
390     }
391 
392     public PurgePolicy getPurgePolicy() {
393         return purgePolicy;
394     }
395 
396     public RewritePolicy getRewritePolicy() {
397         return rewritePolicy;
398     }
399 
400     public Routes getRoutes() {
401         return routes;
402     }
403 
404     public Configuration getConfiguration() {
405         return configuration;
406     }
407 
408     public ConcurrentMap<Object, Object> getScriptStaticVariables() {
409         return scriptStaticVariables;
410     }
411 
412     /**
413      * LOG4J2-2629: PurgePolicy implementations can invoke {@link #deleteAppender(String)} after we have looked up
414      * an instance of a target appender but before events are appended, which could result in events not being
415      * recorded to any appender.
416      * This extension of {@link AppenderControl} allows to to mark usage of an appender, allowing deferral of
417      * {@link Appender#stop()} until events have successfully been recorded.
418      * Alternative approaches considered:
419      * - More aggressive synchronization: Appenders may do expensive I/O that shouldn't block routing.
420      * - Move the 'updatePurgePolicy' invocation before appenders are called: Unfortunately this approach doesn't work
421      *   if we consider an ImmediatePurgePolicy (or IdlePurgePolicy with a very small timeout) because it may attempt
422      *   to remove an appender that doesn't exist yet. It's counterintuitive to get an event that a route has been
423      *   used at a point when we expect the route doesn't exist in {@link #getAppenders()}.
424      */
425     private static abstract class RouteAppenderControl extends AppenderControl {
426 
427         RouteAppenderControl(Appender appender) {
428             super(appender, null, null);
429         }
430 
431         abstract void checkout();
432 
433         abstract void release();
434     }
435 
436     private static final class CreatedRouteAppenderControl extends RouteAppenderControl {
437 
438         private volatile boolean pendingDeletion = false;
439         private final AtomicInteger depth = new AtomicInteger();
440 
441         CreatedRouteAppenderControl(Appender appender) {
442             super(appender);
443         }
444 
445         @Override
446         void checkout() {
447             if (pendingDeletion) {
448                 LOGGER.warn("CreatedRouteAppenderControl.checkout invoked on a " +
449                         "RouteAppenderControl that is pending deletion");
450             }
451             depth.incrementAndGet();
452         }
453 
454         @Override
455         void release() {
456             depth.decrementAndGet();
457             tryStopAppender();
458         }
459 
460         void tryStopAppender() {
461             if (pendingDeletion
462                     // Only attempt to stop the appender if we can CaS the depth away from zero, otherwise either
463                     // 1. Another invocation of tryStopAppender has succeeded, or
464                     // 2. Events are being appended, and will trigger stop when they complete
465                     && depth.compareAndSet(0, -100_000)) {
466                 Appender appender = getAppender();
467                 LOGGER.debug("Stopping appender {}", appender);
468                 appender.stop();
469             }
470         }
471     }
472 
473     private static final class ReferencedRouteAppenderControl extends RouteAppenderControl {
474 
475         ReferencedRouteAppenderControl(Appender appender) {
476             super(appender);
477         }
478 
479         @Override
480         void checkout() {
481             // nop
482         }
483 
484         @Override
485         void release() {
486             // nop
487         }
488     }
489 }