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.log4j.net; 019 020import org.apache.log4j.AppenderSkeleton; 021import org.apache.log4j.helpers.LogLog; 022import org.apache.log4j.spi.ErrorCode; 023import org.apache.log4j.spi.LoggingEvent; 024 025import javax.jms.JMSException; 026import javax.jms.ObjectMessage; 027import javax.jms.Session; 028import javax.jms.Topic; 029import javax.jms.TopicConnection; 030import javax.jms.TopicConnectionFactory; 031import javax.jms.TopicPublisher; 032import javax.jms.TopicSession; 033import javax.naming.Context; 034import javax.naming.InitialContext; 035import javax.naming.NameNotFoundException; 036import javax.naming.NamingException; 037import java.util.Properties; 038 039/** 040 * A simple appender that publishes events to a JMS Topic. The events 041 * are serialized and transmitted as JMS message type {@link 042 * ObjectMessage}. 043 044 * <p>JMS {@link Topic topics} and {@link TopicConnectionFactory topic 045 * connection factories} are administered objects that are retrieved 046 * using JNDI messaging which in turn requires the retrieval of a JNDI 047 * {@link Context}. 048 049 * <p>There are two common methods for retrieving a JNDI {@link 050 * Context}. If a file resource named <em>jndi.properties</em> is 051 * available to the JNDI API, it will use the information found 052 * therein to retrieve an initial JNDI context. To obtain an initial 053 * context, your code will simply call: 054 055 <pre> 056 InitialContext jndiContext = new InitialContext(); 057 </pre> 058 059 * <p>Calling the no-argument <code>InitialContext()</code> method 060 * will also work from within Enterprise Java Beans (EJBs) because it 061 * is part of the EJB contract for application servers to provide each 062 * bean an environment naming context (ENC). 063 064 * <p>In the second approach, several predetermined properties are set 065 * and these properties are passed to the <code>InitialContext</code> 066 * constructor to connect to the naming service provider. For example, 067 * to connect to JBoss naming service one would write: 068 069<pre> 070 Properties env = new Properties( ); 071 env.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory"); 072 env.put(Context.PROVIDER_URL, "jnp://hostname:1099"); 073 env.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces"); 074 InitialContext jndiContext = new InitialContext(env); 075</pre> 076 077 * where <em>hostname</em> is the host where the JBoss application 078 * server is running. 079 * 080 * <p>To connect to the the naming service of Weblogic application 081 * server one would write: 082 083<pre> 084 Properties env = new Properties( ); 085 env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory"); 086 env.put(Context.PROVIDER_URL, "t3://localhost:7001"); 087 InitialContext jndiContext = new InitialContext(env); 088</pre> 089 090 * <p>Other JMS providers will obviously require different values. 091 * 092 * The initial JNDI context can be obtained by calling the 093 * no-argument <code>InitialContext()</code> method in EJBs. Only 094 * clients running in a separate JVM need to be concerned about the 095 * <em>jndi.properties</em> file and calling {@link 096 * InitialContext#InitialContext()} or alternatively correctly 097 * setting the different properties before calling {@link 098 * InitialContext#InitialContext(java.util.Hashtable)} method. 099 100 101 @author Ceki Gülcü */ 102public class JMSAppender extends AppenderSkeleton { 103 104 String securityPrincipalName; 105 String securityCredentials; 106 String initialContextFactoryName; 107 String urlPkgPrefixes; 108 String providerURL; 109 String topicBindingName; 110 String tcfBindingName; 111 String userName; 112 String password; 113 boolean locationInfo; 114 115 TopicConnection topicConnection; 116 TopicSession topicSession; 117 TopicPublisher topicPublisher; 118 119 public 120 JMSAppender() { 121 } 122 123 /** 124 The <b>TopicConnectionFactoryBindingName</b> option takes a 125 string value. Its value will be used to lookup the appropriate 126 <code>TopicConnectionFactory</code> from the JNDI context. 127 */ 128 public 129 void setTopicConnectionFactoryBindingName(String tcfBindingName) { 130 this.tcfBindingName = tcfBindingName; 131 } 132 133 /** 134 Returns the value of the <b>TopicConnectionFactoryBindingName</b> option. 135 */ 136 public 137 String getTopicConnectionFactoryBindingName() { 138 return tcfBindingName; 139 } 140 141 /** 142 The <b>TopicBindingName</b> option takes a 143 string value. Its value will be used to lookup the appropriate 144 <code>Topic</code> from the JNDI context. 145 */ 146 public 147 void setTopicBindingName(String topicBindingName) { 148 this.topicBindingName = topicBindingName; 149 } 150 151 /** 152 Returns the value of the <b>TopicBindingName</b> option. 153 */ 154 public 155 String getTopicBindingName() { 156 return topicBindingName; 157 } 158 159 160 /** 161 Returns value of the <b>LocationInfo</b> property which 162 determines whether location (stack) info is sent to the remote 163 subscriber. */ 164 public 165 boolean getLocationInfo() { 166 return locationInfo; 167 } 168 169 /** 170 * Options are activated and become effective only after calling 171 * this method.*/ 172 public void activateOptions() { 173 TopicConnectionFactory topicConnectionFactory; 174 175 try { 176 Context jndi; 177 178 LogLog.debug("Getting initial context."); 179 if(initialContextFactoryName != null) { 180 Properties env = new Properties( ); 181 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName); 182 if(providerURL != null) { 183 env.put(Context.PROVIDER_URL, providerURL); 184 } else { 185 LogLog.warn("You have set InitialContextFactoryName option but not the " 186 +"ProviderURL. This is likely to cause problems."); 187 } 188 if(urlPkgPrefixes != null) { 189 env.put(Context.URL_PKG_PREFIXES, urlPkgPrefixes); 190 } 191 192 if(securityPrincipalName != null) { 193 env.put(Context.SECURITY_PRINCIPAL, securityPrincipalName); 194 if(securityCredentials != null) { 195 env.put(Context.SECURITY_CREDENTIALS, securityCredentials); 196 } else { 197 LogLog.warn("You have set SecurityPrincipalName option but not the " 198 +"SecurityCredentials. This is likely to cause problems."); 199 } 200 } 201 jndi = new InitialContext(env); 202 } else { 203 jndi = new InitialContext(); 204 } 205 206 LogLog.debug("Looking up ["+tcfBindingName+"]"); 207 topicConnectionFactory = (TopicConnectionFactory) lookup(jndi, tcfBindingName); 208 LogLog.debug("About to create TopicConnection."); 209 if(userName != null) { 210 topicConnection = topicConnectionFactory.createTopicConnection(userName, 211 password); 212 } else { 213 topicConnection = topicConnectionFactory.createTopicConnection(); 214 } 215 216 LogLog.debug("Creating TopicSession, non-transactional, " 217 +"in AUTO_ACKNOWLEDGE mode."); 218 topicSession = topicConnection.createTopicSession(false, 219 Session.AUTO_ACKNOWLEDGE); 220 221 LogLog.debug("Looking up topic name ["+topicBindingName+"]."); 222 Topic topic = (Topic) lookup(jndi, topicBindingName); 223 224 LogLog.debug("Creating TopicPublisher."); 225 topicPublisher = topicSession.createPublisher(topic); 226 227 LogLog.debug("Starting TopicConnection."); 228 topicConnection.start(); 229 230 jndi.close(); 231 } catch(JMSException e) { 232 errorHandler.error("Error while activating options for appender named ["+name+ 233 "].", e, ErrorCode.GENERIC_FAILURE); 234 } catch(NamingException e) { 235 errorHandler.error("Error while activating options for appender named ["+name+ 236 "].", e, ErrorCode.GENERIC_FAILURE); 237 } catch(RuntimeException e) { 238 errorHandler.error("Error while activating options for appender named ["+name+ 239 "].", e, ErrorCode.GENERIC_FAILURE); 240 } 241 } 242 243 protected Object lookup(Context ctx, String name) throws NamingException { 244 try { 245 return ctx.lookup(name); 246 } catch(NameNotFoundException e) { 247 LogLog.error("Could not find name ["+name+"]."); 248 throw e; 249 } 250 } 251 252 protected boolean checkEntryConditions() { 253 String fail = null; 254 255 if(this.topicConnection == null) { 256 fail = "No TopicConnection"; 257 } else if(this.topicSession == null) { 258 fail = "No TopicSession"; 259 } else if(this.topicPublisher == null) { 260 fail = "No TopicPublisher"; 261 } 262 263 if(fail != null) { 264 errorHandler.error(fail +" for JMSAppender named ["+name+"]."); 265 return false; 266 } else { 267 return true; 268 } 269 } 270 271 /** 272 Close this JMSAppender. Closing releases all resources used by the 273 appender. A closed appender cannot be re-opened. */ 274 public synchronized void close() { 275 // The synchronized modifier avoids concurrent append and close operations 276 277 if(this.closed) 278 return; 279 280 LogLog.debug("Closing appender ["+name+"]."); 281 this.closed = true; 282 283 try { 284 if(topicSession != null) 285 topicSession.close(); 286 if(topicConnection != null) 287 topicConnection.close(); 288 } catch(JMSException e) { 289 LogLog.error("Error while closing JMSAppender ["+name+"].", e); 290 } catch(RuntimeException e) { 291 LogLog.error("Error while closing JMSAppender ["+name+"].", e); 292 } 293 // Help garbage collection 294 topicPublisher = null; 295 topicSession = null; 296 topicConnection = null; 297 } 298 299 /** 300 This method called by {@link AppenderSkeleton#doAppend} method to 301 do most of the real appending work. */ 302 public void append(LoggingEvent event) { 303 if(!checkEntryConditions()) { 304 return; 305 } 306 307 try { 308 ObjectMessage msg = topicSession.createObjectMessage(); 309 if(locationInfo) { 310 event.getLocationInformation(); 311 } 312 msg.setObject(event); 313 topicPublisher.publish(msg); 314 } catch(JMSException e) { 315 errorHandler.error("Could not publish message in JMSAppender ["+name+"].", e, 316 ErrorCode.GENERIC_FAILURE); 317 } catch(RuntimeException e) { 318 errorHandler.error("Could not publish message in JMSAppender ["+name+"].", e, 319 ErrorCode.GENERIC_FAILURE); 320 } 321 } 322 323 /** 324 * Returns the value of the <b>InitialContextFactoryName</b> option. 325 * See {@link #setInitialContextFactoryName} for more details on the 326 * meaning of this option. 327 * */ 328 public String getInitialContextFactoryName() { 329 return initialContextFactoryName; 330 } 331 332 /** 333 * Setting the <b>InitialContextFactoryName</b> method will cause 334 * this <code>JMSAppender</code> instance to use the {@link 335 * InitialContext#InitialContext(Hashtable)} method instead of the 336 * no-argument constructor. If you set this option, you should also 337 * at least set the <b>ProviderURL</b> option. 338 * 339 * <p>See also {@link #setProviderURL(String)}. 340 * */ 341 public void setInitialContextFactoryName(String initialContextFactoryName) { 342 this.initialContextFactoryName = initialContextFactoryName; 343 } 344 345 public String getProviderURL() { 346 return providerURL; 347 } 348 349 public void setProviderURL(String providerURL) { 350 this.providerURL = providerURL; 351 } 352 353 String getURLPkgPrefixes( ) { 354 return urlPkgPrefixes; 355 } 356 357 public void setURLPkgPrefixes(String urlPkgPrefixes ) { 358 this.urlPkgPrefixes = urlPkgPrefixes; 359 } 360 361 public String getSecurityCredentials() { 362 return securityCredentials; 363 } 364 365 public void setSecurityCredentials(String securityCredentials) { 366 this.securityCredentials = securityCredentials; 367 } 368 369 370 public String getSecurityPrincipalName() { 371 return securityPrincipalName; 372 } 373 374 public void setSecurityPrincipalName(String securityPrincipalName) { 375 this.securityPrincipalName = securityPrincipalName; 376 } 377 378 public String getUserName() { 379 return userName; 380 } 381 382 /** 383 * The user name to use when {@link 384 * TopicConnectionFactory#createTopicConnection(String, String) 385 * creating a topic session}. If you set this option, you should 386 * also set the <b>Password</b> option. See {@link 387 * #setPassword(String)}. 388 * */ 389 public void setUserName(String userName) { 390 this.userName = userName; 391 } 392 393 public String getPassword() { 394 return password; 395 } 396 397 /** 398 * The paswword to use when creating a topic session. 399 */ 400 public void setPassword(String password) { 401 this.password = password; 402 } 403 404 405 /** 406 If true, the information sent to the remote subscriber will 407 include caller's location information. By default no location 408 information is sent to the subscriber. */ 409 public void setLocationInfo(boolean locationInfo) { 410 this.locationInfo = locationInfo; 411 } 412 413 /** 414 * Returns the TopicConnection used for this appender. Only valid after 415 * activateOptions() method has been invoked. 416 */ 417 protected TopicConnection getTopicConnection() { 418 return topicConnection; 419 } 420 421 /** 422 * Returns the TopicSession used for this appender. Only valid after 423 * activateOptions() method has been invoked. 424 */ 425 protected TopicSession getTopicSession() { 426 return topicSession; 427 } 428 429 /** 430 * Returns the TopicPublisher used for this appender. Only valid after 431 * activateOptions() method has been invoked. 432 */ 433 protected TopicPublisher getTopicPublisher() { 434 return topicPublisher; 435 } 436 437 /** 438 * The JMSAppender sends serialized events and consequently does not 439 * require a layout. 440 */ 441 public boolean requiresLayout() { 442 return false; 443 } 444}