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}