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 */ 017 018package org.apache.logging.log4j.core.appender.mom; 019 020import java.io.Serializable; 021import java.util.Properties; 022import java.util.concurrent.CountDownLatch; 023import java.util.concurrent.TimeUnit; 024 025import javax.jms.Connection; 026import javax.jms.ConnectionFactory; 027import javax.jms.Destination; 028import javax.jms.JMSException; 029import javax.jms.MapMessage; 030import javax.jms.Message; 031import javax.jms.MessageConsumer; 032import javax.jms.MessageProducer; 033import javax.jms.Session; 034import javax.naming.NamingException; 035 036import org.apache.logging.log4j.Logger; 037import org.apache.logging.log4j.core.LogEvent; 038import org.apache.logging.log4j.core.appender.AbstractManager; 039import org.apache.logging.log4j.core.appender.AppenderLoggingException; 040import org.apache.logging.log4j.core.appender.ManagerFactory; 041import org.apache.logging.log4j.core.net.JndiManager; 042import org.apache.logging.log4j.core.util.Log4jThread; 043import org.apache.logging.log4j.status.StatusLogger; 044import org.apache.logging.log4j.util.BiConsumer; 045 046/** 047 * Consider this class <b>private</b>; it is only <b>public</b> for access by integration tests. 048 * 049 * <p> 050 * JMS connection and session manager. Can be used to access MessageProducer, MessageConsumer, and Message objects 051 * involving a configured ConnectionFactory and Destination. 052 * </p> 053 */ 054public class JmsManager extends AbstractManager { 055 056 public static class JmsManagerConfiguration { 057 private final Properties jndiProperties; 058 private final String connectionFactoryName; 059 private final String destinationName; 060 private final String userName; 061 private final char[] password; 062 private final boolean immediateFail; 063 private final boolean retry; 064 private final long reconnectIntervalMillis; 065 066 JmsManagerConfiguration(final Properties jndiProperties, final String connectionFactoryName, 067 final String destinationName, final String userName, final char[] password, final boolean immediateFail, 068 final long reconnectIntervalMillis) { 069 this.jndiProperties = jndiProperties; 070 this.connectionFactoryName = connectionFactoryName; 071 this.destinationName = destinationName; 072 this.userName = userName; 073 this.password = password; 074 this.immediateFail = immediateFail; 075 this.reconnectIntervalMillis = reconnectIntervalMillis; 076 this.retry = reconnectIntervalMillis > 0; 077 } 078 079 public String getConnectionFactoryName() { 080 return connectionFactoryName; 081 } 082 083 public String getDestinationName() { 084 return destinationName; 085 } 086 087 public JndiManager getJndiManager() { 088 return JndiManager.getJndiManager(getJndiProperties()); 089 } 090 091 public Properties getJndiProperties() { 092 return jndiProperties; 093 } 094 095 public char[] getPassword() { 096 return password; 097 } 098 099 public long getReconnectIntervalMillis() { 100 return reconnectIntervalMillis; 101 } 102 103 public String getUserName() { 104 return userName; 105 } 106 107 public boolean isImmediateFail() { 108 return immediateFail; 109 } 110 111 public boolean isRetry() { 112 return retry; 113 } 114 115 @Override 116 public String toString() { 117 return "JmsManagerConfiguration [jndiProperties=" + jndiProperties + ", connectionFactoryName=" 118 + connectionFactoryName + ", destinationName=" + destinationName + ", userName=" + userName 119 + ", immediateFail=" + immediateFail + ", retry=" + retry + ", reconnectIntervalMillis=" 120 + reconnectIntervalMillis + "]"; 121 } 122 123 } 124 125 private static class JmsManagerFactory implements ManagerFactory<JmsManager, JmsManagerConfiguration> { 126 127 @Override 128 public JmsManager createManager(final String name, final JmsManagerConfiguration data) { 129 try { 130 return new JmsManager(name, data); 131 } catch (final Exception e) { 132 LOGGER.error("Error creating JmsManager using JmsManagerConfiguration [{}]", data, e); 133 return null; 134 } 135 } 136 } 137 138 /** 139 * Handles reconnecting to a Socket on a Thread. 140 */ 141 private class Reconnector extends Log4jThread { 142 143 private final CountDownLatch latch = new CountDownLatch(1); 144 145 private volatile boolean shutdown = false; 146 147 private final Object owner; 148 149 public Reconnector(final Object owner) { 150 super("JmsManager-Reconnector"); 151 this.owner = owner; 152 } 153 154 public void latch() { 155 try { 156 latch.await(); 157 } catch (final InterruptedException ex) { 158 // Ignore the exception. 159 } 160 } 161 162 void reconnect() throws NamingException, JMSException { 163 final JndiManager jndiManager2 = getJndiManager(); 164 final Connection connection2 = createConnection(jndiManager2); 165 final Session session2 = createSession(connection2); 166 final Destination destination2 = createDestination(jndiManager2); 167 final MessageProducer messageProducer2 = createMessageProducer(session2, destination2); 168 connection2.start(); 169 synchronized (owner) { 170 jndiManager = jndiManager2; 171 connection = connection2; 172 session = session2; 173 destination = destination2; 174 messageProducer = messageProducer2; 175 reconnector = null; 176 shutdown = true; 177 } 178 LOGGER.debug("Connection reestablished to {}", configuration); 179 } 180 181 @Override 182 public void run() { 183 while (!shutdown) { 184 try { 185 sleep(configuration.getReconnectIntervalMillis()); 186 reconnect(); 187 } catch (final InterruptedException | JMSException | NamingException e) { 188 LOGGER.debug("Cannot reestablish JMS connection to {}: {}", configuration, e.getLocalizedMessage(), 189 e); 190 } finally { 191 latch.countDown(); 192 } 193 } 194 } 195 196 public void shutdown() { 197 shutdown = true; 198 } 199 200 } 201 202 private static final Logger LOGGER = StatusLogger.getLogger(); 203 204 static final JmsManagerFactory FACTORY = new JmsManagerFactory(); 205 206 /** 207 * Gets a JmsManager using the specified configuration parameters. 208 * 209 * @param name 210 * The name to use for this JmsManager. 211 * @param connectionFactoryName 212 * The binding name for the {@link javax.jms.ConnectionFactory}. 213 * @param destinationName 214 * The binding name for the {@link javax.jms.Destination}. 215 * @param userName 216 * The userName to connect with or {@code null} for no authentication. 217 * @param password 218 * The password to use with the given userName or {@code null} for no authentication. 219 * @param immediateFail 220 * Whether or not to fail immediately with a {@link AppenderLoggingException} when connecting to JMS 221 * fails. 222 * @param reconnectIntervalMillis 223 * How to log sleep in milliseconds before trying to reconnect to JMS. 224 * @param jndiManager 225 * The JndiManager to look up JMS information through. 226 * @return The JmsManager as configured. 227 */ 228 public static JmsManager getJmsManager(final String name, final Properties jndiProperties, 229 final String connectionFactoryName, final String destinationName, final String userName, 230 final char[] password, final boolean immediateFail, final long reconnectIntervalMillis) { 231 final JmsManagerConfiguration configuration = new JmsManagerConfiguration(jndiProperties, connectionFactoryName, 232 destinationName, userName, password, immediateFail, reconnectIntervalMillis); 233 return getManager(name, FACTORY, configuration); 234 } 235 236 private final JmsManagerConfiguration configuration; 237 238 private volatile Reconnector reconnector; 239 private volatile JndiManager jndiManager; 240 private volatile Connection connection; 241 private volatile Session session; 242 private volatile Destination destination; 243 private volatile MessageProducer messageProducer; 244 245 private JmsManager(final String name, final JmsManagerConfiguration configuration) { 246 super(null, name); 247 this.configuration = configuration; 248 this.jndiManager = configuration.getJndiManager(); 249 try { 250 this.connection = createConnection(this.jndiManager); 251 this.session = createSession(this.connection); 252 this.destination = createDestination(this.jndiManager); 253 this.messageProducer = createMessageProducer(this.session, this.destination); 254 this.connection.start(); 255 } catch (NamingException | JMSException e) { 256 this.reconnector = createReconnector(); 257 this.reconnector.start(); 258 } 259 } 260 261 private boolean closeConnection() { 262 if (connection == null) { 263 return true; 264 } 265 final Connection temp = connection; 266 connection = null; 267 try { 268 temp.close(); 269 return true; 270 } catch (final JMSException e) { 271 StatusLogger.getLogger().debug( 272 "Caught exception closing JMS Connection: {} ({}); continuing JMS manager shutdown", 273 e.getLocalizedMessage(), temp, e); 274 return false; 275 } 276 } 277 278 private boolean closeJndiManager() { 279 if (jndiManager == null) { 280 return true; 281 } 282 final JndiManager tmp = jndiManager; 283 jndiManager = null; 284 tmp.close(); 285 return true; 286 } 287 288 private boolean closeMessageProducer() { 289 if (messageProducer == null) { 290 return true; 291 } 292 final MessageProducer temp = messageProducer; 293 messageProducer = null; 294 try { 295 temp.close(); 296 return true; 297 } catch (final JMSException e) { 298 StatusLogger.getLogger().debug( 299 "Caught exception closing JMS MessageProducer: {} ({}); continuing JMS manager shutdown", 300 e.getLocalizedMessage(), temp, e); 301 return false; 302 } 303 } 304 305 private boolean closeSession() { 306 if (session == null) { 307 return true; 308 } 309 final Session temp = session; 310 session = null; 311 try { 312 temp.close(); 313 return true; 314 } catch (final JMSException e) { 315 StatusLogger.getLogger().debug( 316 "Caught exception closing JMS Session: {} ({}); continuing JMS manager shutdown", 317 e.getLocalizedMessage(), temp, e); 318 return false; 319 } 320 } 321 322 private Connection createConnection(final JndiManager jndiManager) throws NamingException, JMSException { 323 final ConnectionFactory connectionFactory = jndiManager.lookup(configuration.getConnectionFactoryName()); 324 if (configuration.getUserName() != null && configuration.getPassword() != null) { 325 return connectionFactory.createConnection(configuration.getUserName(), 326 configuration.getPassword() == null ? null : String.valueOf(configuration.getPassword())); 327 } 328 return connectionFactory.createConnection(); 329 330 } 331 332 private Destination createDestination(final JndiManager jndiManager) throws NamingException { 333 return jndiManager.lookup(configuration.getDestinationName()); 334 } 335 336 /** 337 * Creates a TextMessage, MapMessage, or ObjectMessage from a Serializable object. 338 * <p> 339 * For instance, when using a text-based {@link org.apache.logging.log4j.core.Layout} such as 340 * {@link org.apache.logging.log4j.core.layout.PatternLayout}, the {@link org.apache.logging.log4j.core.LogEvent} 341 * message will be serialized to a String. 342 * </p> 343 * <p> 344 * When using a layout such as {@link org.apache.logging.log4j.core.layout.SerializedLayout}, the LogEvent message 345 * will be serialized as a Java object. 346 * </p> 347 * <p> 348 * When using a layout such as {@link org.apache.logging.log4j.core.layout.MessageLayout} and the LogEvent message 349 * is a Log4j MapMessage, the message will be serialized as a JMS MapMessage. 350 * </p> 351 * 352 * @param object 353 * The LogEvent or String message to wrap. 354 * @return A new JMS message containing the provided object. 355 * @throws JMSException 356 */ 357 public Message createMessage(final Serializable object) throws JMSException { 358 if (object instanceof String) { 359 return this.session.createTextMessage((String) object); 360 } else if (object instanceof org.apache.logging.log4j.message.MapMessage) { 361 return map((org.apache.logging.log4j.message.MapMessage<?, ?>) object, this.session.createMapMessage()); 362 } 363 return this.session.createObjectMessage(object); 364 } 365 366 private void createMessageAndSend(final LogEvent event, final Serializable serializable) throws JMSException { 367 final Message message = createMessage(serializable); 368 message.setJMSTimestamp(event.getTimeMillis()); 369 messageProducer.send(message); 370 } 371 372 /** 373 * Creates a MessageConsumer on this Destination using the current Session. 374 * 375 * @return A MessageConsumer on this Destination. 376 * @throws JMSException 377 */ 378 public MessageConsumer createMessageConsumer() throws JMSException { 379 return this.session.createConsumer(this.destination); 380 } 381 382 /** 383 * Creates a MessageProducer on this Destination using the current Session. 384 * 385 * @param session 386 * The JMS Session to use to create the MessageProducer 387 * @param destination 388 * The JMS Destination for the MessageProducer 389 * @return A MessageProducer on this Destination. 390 * @throws JMSException 391 */ 392 public MessageProducer createMessageProducer(final Session session, final Destination destination) 393 throws JMSException { 394 return session.createProducer(destination); 395 } 396 397 private Reconnector createReconnector() { 398 final Reconnector recon = new Reconnector(this); 399 recon.setDaemon(true); 400 recon.setPriority(Thread.MIN_PRIORITY); 401 return recon; 402 } 403 404 private Session createSession(final Connection connection) throws JMSException { 405 return connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 406 } 407 408 public JmsManagerConfiguration getJmsManagerConfiguration() { 409 return configuration; 410 } 411 412 JndiManager getJndiManager() { 413 return configuration.getJndiManager(); 414 } 415 416 <T> T lookup(final String destinationName) throws NamingException { 417 return this.jndiManager.lookup(destinationName); 418 } 419 420 private MapMessage map(final org.apache.logging.log4j.message.MapMessage<?, ?> log4jMapMessage, 421 final MapMessage jmsMapMessage) { 422 // Map without calling rg.apache.logging.log4j.message.MapMessage#getData() which makes a copy of the map. 423 log4jMapMessage.forEach(new BiConsumer<String, Object>() { 424 @Override 425 public void accept(final String key, final Object value) { 426 try { 427 jmsMapMessage.setObject(key, value); 428 } catch (final JMSException e) { 429 throw new IllegalArgumentException(String.format("%s mapping key '%s' to value '%s': %s", 430 e.getClass(), key, value, e.getLocalizedMessage()), e); 431 } 432 } 433 }); 434 return jmsMapMessage; 435 } 436 437 @Override 438 protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) { 439 if (reconnector != null) { 440 reconnector.shutdown(); 441 reconnector.interrupt(); 442 reconnector = null; 443 } 444 boolean closed = false; 445 closed &= closeJndiManager(); 446 closed &= closeMessageProducer(); 447 closed &= closeSession(); 448 closed &= closeConnection(); 449 return closed && this.jndiManager.stop(timeout, timeUnit); 450 } 451 452 void send(final LogEvent event, final Serializable serializable) { 453 if (messageProducer == null) { 454 if (reconnector != null && !configuration.isImmediateFail()) { 455 reconnector.latch(); 456 } 457 if (messageProducer == null) { 458 throw new AppenderLoggingException( 459 "Error sending to JMS Manager '" + getName() + "': JMS message producer not available"); 460 } 461 } 462 synchronized (this) { 463 try { 464 createMessageAndSend(event, serializable); 465 } catch (final JMSException causeEx) { 466 if (configuration.isRetry() && reconnector == null) { 467 reconnector = createReconnector(); 468 try { 469 closeJndiManager(); 470 reconnector.reconnect(); 471 } catch (NamingException | JMSException reconnEx) { 472 LOGGER.debug("Cannot reestablish JMS connection to {}: {}; starting reconnector thread {}", 473 configuration, reconnEx.getLocalizedMessage(), reconnector.getName(), reconnEx); 474 reconnector.start(); 475 throw new AppenderLoggingException( 476 String.format("Error sending to %s for %s", getName(), configuration), causeEx); 477 } 478 try { 479 createMessageAndSend(event, serializable); 480 } catch (final JMSException e) { 481 throw new AppenderLoggingException( 482 String.format("Error sending to %s after reestablishing connection for %s", getName(), 483 configuration), 484 causeEx); 485 } 486 } 487 } 488 } 489 } 490 491}