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}