Java tutorial
/*======================================================================* * Copyright (c) 2008, Yahoo! Inc. All rights reserved. * * * * Licensed under the New BSD License (the "License"); you may not use * * this file except in compliance with the License. Unless required * * by applicable law or agreed to in writing, software distributed * * under the License is distributed on an "AS IS" BASIS, WITHOUT * * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * * limitations under the License. See accompanying LICENSE file. * *======================================================================*/ package org.mondemand; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Pattern; import org.mondemand.transport.LWESTransport; import org.mondemand.util.ClassUtils; import com.google.common.util.concurrent.AtomicLongMap; /** * This is the main entry point to MonDemand. Users can create client objects and use them to * log messages and statistics. * @author Michael Lum * */ public class Client { /******************************** * CONSTANTS * ********************************/ private final static String FILE_LINE_DELIMITER = ":"; private final static int CALLER_DEPTH = 3; private final static int MAX_MESSAGES = 10; private static final String TRACE_KEY = "mondemand.trace_id"; private static final String OWNER_KEY = "mondemand.owner"; private static final String MESSAGE_KEY = "mondemand.message"; private static final String CONFIG_FILE = "/etc/mondemand/mondemand.conf"; private static final int EMIT_INTERVAL = 60; // 60 seconds private static final boolean DEFAULT_AUTO_EMIT = false; // auto emit disabled by default private static final boolean DEFAULT_CLEAR_STAT = false; // clear stats after flush by auto emit private static final Pattern keyPattern = Pattern.compile("[\\w\\.-]+"); // valid values for key: a-z A-Z 0-9 _ - . /******************************** * CLASS ATTRIBUTES * ********************************/ private ErrorHandler errorHandler = new DefaultErrorHandler(); private String programId = null; private int immediateSendLevel = Level.CRIT; private int noSendLevel = Level.ALL; private ConcurrentHashMap<String, Context> contexts = null; private ConcurrentHashMap<String, LogMessage> messages = null; private ConcurrentHashMap<String, StatsMessage> stats = null; private ConcurrentHashMap<String, SamplesMessage> samples = null; private ConcurrentHashMap<ContextList, AtomicLongMap<String>> contextStats = null; private ConcurrentHashMap<EventType, List<Transport>> transports = null; private ClientStatEmitter autoStatEmitter = null; private Thread emitterThread = null; private Integer maxNumMetrics = null; // key is the examined key, value specifies if the key is valid or not static private ConcurrentHashMap<String, Boolean> examinedKeys = new ConcurrentHashMap<String, Boolean>(); /******************************** * CONSTRUCTORS AND DESTRUCTORS * ********************************/ /** * object to auto emit the stats and logs */ public class ClientStatEmitter implements Runnable { private final Client client; // client to emit stats for private final int intervalMS; // emit interval in milli seconds private final boolean clearStats; // if the stats should be cleared after flush private volatile boolean stop = false; // if the emitter should stop /** * constructor * @param client - the client to emit stats for * @param interval - emission interval in seconds * @param clearStats - whether or not stats should be cleared after emit */ ClientStatEmitter(Client client, int interval, boolean clearStats) { this.client = client; this.intervalMS = interval * 1000; this.clearStats = clearStats; } /** * make the auto-emit thread stop */ public void stop() { stop = true; } /** * the run method for the emitter thread. it would sleep for interval and * then emit the stats, and keep doing the same until it is interrupted to * stop */ @Override public void run() { while (!stop) { try { Thread.sleep(intervalMS); client.flush(clearStats); } catch (InterruptedException e) { // if we are interrupted, it will check for the stop flag } } // final flush client.flush(clearStats); } } /** * The constructor creates a Client object that is ready to use. * * @param programId a string identifying the program that is calling MonDemand * @param autoStatEmit specifies if auto stat emit is enabled * @param clearStatAfterEmit specifies if stats should be cleared after each auto emit * @param statEmitInterval - auto stat emit interval, ignored if auto * emission is disabled */ public Client(String programId, boolean autoStatEmit, boolean clearStatAfterEmit, int statEmitInterval) { /* set the default error handler */ this.errorHandler = new DefaultErrorHandler(); /* if the program ID is not specified, try to get it from the stack */ if (programId == null) { this.programId = ClassUtils.getMainClass(); } else { this.programId = programId; } /* setup internal data structures */ contexts = new ConcurrentHashMap<String, Context>(); messages = new ConcurrentHashMap<String, LogMessage>(); stats = new ConcurrentHashMap<String, StatsMessage>(); samples = new ConcurrentHashMap<String, SamplesMessage>(); transports = new ConcurrentHashMap<EventType, List<Transport>>(); contextStats = new ConcurrentHashMap<ContextList, AtomicLongMap<String>>(); // initialize transports with empty lists for (EventType eventType : EventType.values()) { transports.put(eventType, new CopyOnWriteArrayList<Transport>()); } // create and start the emitter thread if (autoStatEmit) { // make sure interval is greater than 1, otherwise use the default statEmitInterval = statEmitInterval <= 0 ? EMIT_INTERVAL : statEmitInterval; autoStatEmitter = new ClientStatEmitter(this, statEmitInterval, clearStatAfterEmit); emitterThread = new Thread(autoStatEmitter); emitterThread.start(); } } /** * The constructor creates a Client object that is ready to use, uses default * interval of 60 seconds for auto-emit if autoStatEmit is set. * * @param programId a string identifying the program that is calling MonDemand * @param autoStatEmit specifies if auto stat emit is enabled * @param clearStatAfterEmit specifies if stats should be cleared after each auto emit */ public Client(String programId, boolean autoStatEmit, boolean clearStatAfterEmit) { this(programId, autoStatEmit, clearStatAfterEmit, EMIT_INTERVAL); } /** * The constructor creates a Client object that is ready to use, uses default * interval of 60 seconds for auto-emit if autoStatEmit is set. * * @param programId a string identifying the program that is calling MonDemand * @param autoStatEmit specifies if auto stat emit is enabled */ public Client(String programId, boolean autoStatEmit) { this(programId, autoStatEmit, DEFAULT_CLEAR_STAT, EMIT_INTERVAL); } /** * The constructor creates a Client object that is ready to use. the auto-stat * emit for the client created with this constructor is turned off. * * @param programId a string identifying the program that is calling MonDemand */ public Client(String programId) { this(programId, DEFAULT_AUTO_EMIT, DEFAULT_CLEAR_STAT, EMIT_INTERVAL); } /** * The constructor creates a Client object that is ready to use. * @param programId a string identifying the program that is calling Mondemand * @param host a string identifying the host (i.e. InetAddress.getLocalHost().getHostName()) where the program is running. * @throws MondemandException */ public Client(String programId, String host) throws MondemandException { this(programId); addContext("host", host); } /** * Called when the client is destroyed. Ensures that everything is cleaned up properly. */ @Override public void finalize() { // try to flush all logs, stats and samples flush(); // stop the auto-emit thread if (emitterThread != null) { try { autoStatEmitter.stop(); emitterThread.interrupt(); emitterThread.join(); autoStatEmitter = null; emitterThread = null; } catch (Exception e) { // ignore the exception for join } } // clear all the data contexts.clear(); messages.clear(); stats.clear(); samples.clear(); contextStats.clear(); // shutdown all the transports Set<Transport> seenTransports = new HashSet<Transport>(); for (List<Transport> transportList : transports.values()) { for (Transport t : transportList) { if (!seenTransports.contains(t)) { seenTransports.add(t); try { t.shutdown(); } catch (TransportException e) { } } } } } /******************************** * ACCESSORS AND MUTATORS * ********************************/ /** * @return the programId */ public String getProgramId() { return programId; } /** * @param programId the programId to set */ public void setProgramId(String programId) { this.programId = programId; } /** * @return the immediateSendLevel */ public int getImmediateSendLevel() { return immediateSendLevel; } /** * @param immediateSendLevel the immediateSendLevel to set */ public void setImmediateSendLevel(int immediateSendLevel) { this.immediateSendLevel = immediateSendLevel; } /** * @return the noSendLevel */ public int getNoSendLevel() { return noSendLevel; } /** * @param noSendLevel the noSendLevel to set */ public void setNoSendLevel(int noSendLevel) { this.noSendLevel = noSendLevel; } /** * @return the errorHandler */ public ErrorHandler getErrorHandler() { return errorHandler; } /** * Sets a custom error handler. Cowardly refuses to set it to null. * @param errorHandler the errorHandler to set */ public void setErrorHandler(ErrorHandler errorHandler) { if (errorHandler != null) { this.errorHandler = errorHandler; } } /** * @param maxNumMetrics the maximum number of metrics to send in stats messages */ public void setMaxNumMetrics(Integer maxNumMetrics) { this.maxNumMetrics = maxNumMetrics; } /******************************** * PUBLIC API METHODS * ********************************/ /** * Adds contextual data to the client. * @throws MondemandException */ public void addContext(String key, String value) throws MondemandException { Context ctxt = new Context(key, value); if (contexts == null) { contexts = new ConcurrentHashMap<String, Context>(); } if (key != null && value != null) { contexts.put(key, ctxt); } } /** * Removes contextual data from the client. */ public void removeContext(String key) { if (contexts != null && key != null) { contexts.remove(key); } } /** * Fetches contextual data from the client. */ public String getContext(String key) { String retval = null; if (contexts != null && key != null) { Context ctxt = contexts.get(key); if (ctxt != null) { retval = ctxt.getValue(); } } return retval; } /** * Retrieves an enumeration of all the contextual data keys * * @return an enumeration of all keys */ public Enumeration<String> getContextKeys() { if (contexts == null) { contexts = new ConcurrentHashMap<String, Context>(); } return contexts.keys(); } /** * Clear contextual data from the logger. Contextual data persists between * flush() calls and is only removed if you call removeAllContexts(). */ public void removeAllContexts() { if (contexts != null) { contexts.clear(); } } /** * verifies if a key is a valid mondemand key (a-Z A-Z 0-9 _ - .) * * @param key - the input key * @return true if the key consists of valid characters, false if otherwise * or key is empty. */ public static boolean isKeyValid(String key) { if (key == null || key.isEmpty()) { return false; } // look up the map first to avoid calling matcher when possible Boolean valid = examinedKeys.get(key); if (valid == null) { valid = keyPattern.matcher(key).matches(); examinedKeys.putIfAbsent(key, valid); } return valid.booleanValue(); } /** * adds transports from the default configuration file * at "/etc/mondemand/mondemand.conf" * @throws FileNotFoundException if config file could not be found * @throws IOException if config file could not be read * @throws TransportException if there was an error creating the LWESTransport * @throws UnknownHostException if a bad host is specified * @throws IllegalArgumentException if default config file does not exist, * or if there is a problem reading the file, or either port or * address is missing, or if port cannot be converted to number, or if * addresses cannot be converted to valid hosts, or if ttl cannot be * converted to number in valid range, or if sendto cannot be converted * to number in valid range, or if length of port or ttl arrays does * not equal one or length of addr array, or if a transport cannot be * created for addresses/port specified in the file. * @throws NumberFormatException if a bad PORT, TTL, or SENDTO is specified */ public void addTransportsFromDefaultConfigFile() throws FileNotFoundException, IOException, TransportException, UnknownHostException { this.addTransportsFromConfigFile(CONFIG_FILE); } /** * adds transports from a configuration file. the format of the file is: * MONDEMAND_ADDR="<ip>[,<ip>]?" * MONDEMAND_PORT="<port>[,<port>]?" * (optional) MONDEMAND_TTL="<ttl>[,<ttl>]?" * (optional) MONDEMAND_SENDTO="<sendto>" * * @param configFileName - configuration file name. * @throws FileNotFoundException if config file could not be found * @throws IOException if config file could not be read * @throws TransportException if there was an error creating the LWESTransport * @throws UknownHostException if a bad host is specified * @throws IllegalArgumentException if file does not exist, or if there is a * problem reading the file, or either port or address is missing, or * if port cannot be converted to number, or if addresses cannot be * converted to valid hosts, or if ttl cannot be converted to number in * valid range, or if sendto cannot be converted to number in valid * range, or if length of port or ttl arrays does not equal one or * length of addr array, or if a transport cannot be created for * addresses/port specified in the file. * @throws NumberFormatException if a bad PORT, TTL, or SENDTO is specified */ public void addTransportsFromConfigFile(String configFileName) throws FileNotFoundException, IOException, TransportException, UnknownHostException { Properties prop = new Properties(); InputStream input = null; try { // load a properties file input = new FileInputStream(configFileName); prop.load(input); // build config defaults first Config defaults = ConfigBuilder.buildDefaultConfig(prop); // build event-specific configs for (EventType eventType : EventType.values()) { EventSpecificConfig eventSpecific = ConfigBuilder.buildEventSpecificConfig(prop, eventType, defaults); Properties emitterGroupProps = eventSpecific.toEmitterGroupProperties(eventType); // add transport for each event type addTransport(eventType, new LWESTransport(emitterGroupProps, eventType.name())); } } finally { if (input != null) { try { input.close(); } catch (Exception e) { // ignore this exception } } } } /** * Adds a new transport to this client. * @param transport the transport object to add */ public void addTransport(Transport transport) { for (EventType eventType : EventType.values()) { addTransport(eventType, transport); } } /** * Adds a new event-specific transport to this client. * @param eventType the event type for this transport * @param transport the transport object to add */ public void addTransport(EventType eventType, Transport transport) { if (null == transport) { return; } this.transports.get(eventType).add(transport); } /** * A check for the log level that is set. * * @param level the priority level to check * @param traceId the TraceId to check for * @return true if this level is enabled, false otherwise */ public boolean levelIsEnabled(int level, TraceId traceId) { if (traceId == null) { return level < this.noSendLevel; } else { return ((traceId.compareTo(TraceId.NULL_TRACE_ID) != 0) || (level < this.noSendLevel)); } } /** * flushes all logs, stats, and samples to the transports. */ public void flush() { this.flush(false); } /** * flushes all logs, stats, and samples to the transports. * @param resetStats - whether or not stats should be reset after flush */ public void flush(boolean resetStats) { flushLogs(); dispatchStatsSamples(); dispatchContextStats(); if (resetStats) { if (stats != null) { stats.clear(); } if (contextStats != null) { contextStats.clear(); } } } /** * Flushes log data to the transports. */ public void flushLogs() { dispatchLogs(); if (messages != null) { messages.clear(); } } /** * Increments the default counter by one. * @throws MondemandException */ public void increment() throws MondemandException { this.increment(StatType.Counter, null, 1); } /** * Increments the default counter by value * @param value the amount to increment the counter by * @throws MondemandException */ public void increment(int value) throws MondemandException { this.increment(StatType.Counter, null, value); } /** * Increments the specified counter by one. * @param key the name of the counter to increment * @throws MondemandException */ public void increment(String key) throws MondemandException { this.increment(StatType.Counter, key, 1); } /** * Increments the specified counter by the value specified. * @param key the name of the counter to increment * @param value the amount to increment the counter by * @throws MondemandException */ public void increment(String key, int value) throws MondemandException { this.increment(StatType.Counter, key, value); } /** * increment a counter * @param type - type of the counter * @param key - the name of the counter to increment * @param value - the amount to increment the counter by * @throws MondemandException */ public void increment(StatType type, String key, int value) throws MondemandException { String realKey = key; // set the key if (realKey == null) { // determine the key from the calling class and line number realKey = ClassUtils.getCallingClass(CALLER_DEPTH); } if (!isKeyValid(realKey)) { throw new MondemandException("key is invalid: " + realKey); } // create the HashMap if it doesn't exist if (this.stats == null) { this.stats = new ConcurrentHashMap<String, StatsMessage>(); } // Note: increment could be lost due to a race condition but no // synchronization is required. StatsMessage realValue = this.stats.get(realKey); if (realValue == null) { // create the counter if doesn't exist StatsMessage newValue = new StatsMessage(realKey, type); realValue = this.stats.putIfAbsent(realKey, newValue); if (realValue == null) { realValue = newValue; } } // update the counter realValue.incrementBy(value); } /** * Given context&Stats map, increment according to context and key/value * @param context context * @param keyType key * @param value value * @throws MondemandException */ public void increment(ContextList context, String keyType, long value) throws MondemandException { if (!isKeyValid(keyType)) { throw new MondemandException("key is invalid: " + keyType); } // Note: add could be lost due to a race condition but no // synchronization is required. AtomicLongMap<String> stats = contextStats.get(context); if (stats == null) { AtomicLongMap<String> newStats = AtomicLongMap.create(); stats = contextStats.putIfAbsent(context, newStats); if (stats == null) { stats = newStats; } } stats.addAndGet(keyType, value); } /** * Increment the count for the map * @param context : context * @param keyType : KeyType: blank, advertiser_revenue, etc * @throws MondemandException */ public void increment(ContextList context, String keyType) throws MondemandException { increment(context, keyType, 1); } /** * adds a new sample * @param key - the name of the sample to add a new value to * @param value - the amount to be added to sample * @param trackingTypeValue - bitwise value, specifies what extra stats * (min/max/...) should be kept for a counter */ public void addSample(String key, int value, int trackingTypeValue) throws MondemandException { this.addSample(key, value, trackingTypeValue, 0); } /** * adds a new sample * @param key - the name of the sample to add a new value to * @param value - the amount to be added to sample * @param trackingTypeValue - bitwise value, specifies what extra stats * (min/max/...) should be kept for a counter * @param samplesMaxCount - maximum number of samples to keep, ignored if * less than or equal to 0. */ public void addSample(String key, int value, int trackingTypeValue, int samplesMaxCount) throws MondemandException { String realKey = key; // set the key if (realKey == null) { // determine the key from the calling class and line number realKey = ClassUtils.getCallingClass(CALLER_DEPTH); } if (!isKeyValid(realKey)) { throw new MondemandException("key is invalid: " + realKey); } // create the HashMap if it doesn't exist if (this.samples == null) { this.samples = new ConcurrentHashMap<String, SamplesMessage>(); } // Note: sample could be lost if we get the SamplesMessage but fail to // add the sample before the SamplesMessage is emitted. // This approach avoids synchronization though. SamplesMessage realValue = this.samples.get(realKey); if (realValue == null) { // create the counter if doesn't exist SamplesMessage newValue = new SamplesMessage(realKey, trackingTypeValue, samplesMaxCount); realValue = this.samples.putIfAbsent(realKey, newValue); if (realValue == null) { realValue = newValue; } } // update the counter realValue.addSample(value); } /** * Decrements the default counter by one. * @throws MondemandException */ public void decrement() throws MondemandException { this.decrement(StatType.Counter, null, 1); } /** * Decrements the default counter by value * @param value the amount to decrement the counter by * @throws MondemandException */ public void decrement(int value) throws MondemandException { this.decrement(StatType.Counter, null, value); } /** * Decrements the specified counter by one. * @param key the name of the counter to decrement * @throws MondemandException */ public void decrement(String key) throws MondemandException { this.decrement(StatType.Counter, key, 1); } /** * Decrements the specified counter by the value specified. * @param key the name of the counter to decrement * @param value the amount to decrement the counter by * @throws MondemandException */ public void decrement(String key, int value) throws MondemandException { this.decrement(StatType.Counter, key, value); } public void decrement(StatType type, String key, int value) throws MondemandException { this.increment(type, key, value * (-1)); } /** * Sets the counter to the specified val ue. * @param key the name of the counter key to set * @param value the value to set this counter to */ public void setKey(String key, int value) throws MondemandException { this.setKey(StatType.Gauge, key, value); } /** * Sets the counter to the specified value. * @param key the name of the counter key to set * @param value the value to set this counter to */ public void setKey(String key, long value) throws MondemandException { this.setKey(StatType.Gauge, key, value); } public void setKey(StatType type, String key, long value) throws MondemandException { String realKey = key; if (realKey == null) { // determine the key from the calling class and line number realKey = ClassUtils.getCallingClass(CALLER_DEPTH); } if (!isKeyValid(realKey)) { throw new MondemandException("key is invalid: " + realKey); } // create the HashMap if it doesn't exist if (this.stats == null) { this.stats = new ConcurrentHashMap<String, StatsMessage>(); } // create and set the gauge counter, this will overwrite the counter // if it already exists StatsMessage realValue = new StatsMessage(realKey, type); realValue.setCounter(value); this.stats.put(realKey, realValue); } /** * Logs a message at priority level EMERG * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void emerg(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.EMERG, traceId, message, args); } /** * Logs a message a priority level EMERG, determining the calling class and line number * on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void emerg(TraceId traceId, String message, Object[] args) { emerg(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message a log level EMERG. * @param message */ public void emerg(String message) { emerg(null, message, null); } /** * Logs a message a priority level ALERT. * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void alert(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.ALERT, traceId, message, args); } /** * Logs a message a priority level ALERT, determining the calling class and * line number on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void alert(TraceId traceId, String message, Object[] args) { alert(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message at priority level ALERT * @param message the log message */ public void alert(String message) { alert(null, message, null); } /** * Logs a message at priority level CRIT * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void crit(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.CRIT, traceId, message, args); } /** * Logs a message a priority level CRIT, determining the calling class and * line number on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void crit(TraceId traceId, String message, Object[] args) { crit(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message at priority level CRIT * @param message the log message */ public void crit(String message) { crit(null, message, null); } /** * Logs a message at priority level ERROR * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void error(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.ERROR, traceId, message, args); } /** * Logs a message a priority level ERROR, determining the calling class and * line number on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void error(TraceId traceId, String message, Object[] args) { error(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message at priority level ERROR * @param message the log message */ public void error(String message) { error(null, message, null); } /** * Logs a message at priority level WARNING * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void warning(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.WARNING, traceId, message, args); } /** * Logs a message a priority level WARNING, determining the calling class and * line number on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void warning(TraceId traceId, String message, Object[] args) { warning(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message at priority level WARNING * @param message the log message */ public void warning(String message) { warning(null, message, null); } /** * Logs a message at priority level NOTICE * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void notice(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.NOTICE, traceId, message, args); } /** * Logs a message a priority level NOTICE, determining the calling class and * line number on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void notice(TraceId traceId, String message, Object[] args) { notice(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message at priority level NOTICE * @param message the log message */ public void notice(String message) { notice(null, message, null); } /** * Logs a message at priority level INFO * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void info(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.INFO, traceId, message, args); } /** * Logs a message a priority level INFO, determining the calling class and * line number on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void info(TraceId traceId, String message, Object[] args) { info(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message at priority level INFO * @param message the log message */ public void info(String message) { info(null, message, null); } /** * Logs a message at priority level DEBUG * @param name the name of the calling class or filename * @param line the line number of the calling class or filename * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void debug(String name, int line, TraceId traceId, String message, Object[] args) { log(name, line, Level.DEBUG, traceId, message, args); } /** * Logs a message a priority level DEBUG, determining the calling class and * line number on the fly. * @param traceId an optional trace ID * @param message the log message * @param args optional arguments */ public void debug(TraceId traceId, String message, Object[] args) { debug(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), traceId, message, args); } /** * A convenience method to log a message at priority level DEBUG * @param message the log message */ public void debug(String message) { debug(null, message, null); } /** * Generic logger function. This method will perform slower than most because it needs to detect * the calling class and calling line number. * @param level the log level of this message * @param traceId an optional traceId * @param message the log message * @param args optional arguments */ public void log(int level, TraceId traceId, String message, Object[] args) { log(ClassUtils.getCallingClass(CALLER_DEPTH), ClassUtils.getCallingLine(CALLER_DEPTH), level, traceId, message, args); } /** * The most generic logger function. * @param name the name of this message, usually the filename or calling class * @param line the line number calling this message, or other numeric description of the calling class * @param level the log level * @param traceId an optional traceId * @param message the message * @param args optional arguments */ public void log(String name, int line, int level, TraceId traceId, String message, Object[] args) { logReal(name, line, level, traceId, message, args); } public boolean traceMessage(String message, Map<String, String> context) { if (context.containsKey(TRACE_KEY) && context.containsKey(OWNER_KEY)) { return traceMessage(context.get(OWNER_KEY), context.get(TRACE_KEY), message, context); } else { return false; } } public boolean traceMessage(String owner, String traceId, String message, Map<String, String> context) { boolean ret = false; try { List<Context> contextsList = new ArrayList<Context>(context.size() + 3); contextsList.add(new Context(OWNER_KEY, owner)); contextsList.add(new Context(TRACE_KEY, traceId)); contextsList.add(new Context(MESSAGE_KEY, message)); for (Entry<String, String> entry : context.entrySet()) { if (OWNER_KEY.equals(entry.getKey()) || TRACE_KEY.equals(entry.getKey()) || MESSAGE_KEY.equals(entry.getKey())) { // skip anything set in the context with the arguments continue; } contextsList.add(new Context(entry.getKey(), entry.getValue())); } Context[] contexts = contextsList.toArray(new Context[0]); for (Transport t : transports.get(EventType.TRACE)) { try { t.sendTrace(programId, contexts); } catch (TransportException te) { errorHandler.handleError("Error calling Transport.sendTrace()", te); } } ret = true; } catch (Exception e) { errorHandler.handleError("Error calling Client.traceMessage()", e); } return ret; } /** * Send a performance trace message. * @see <a href="https://github.com/mondemand/mondemand.github.com/blob/master/performance_monitoring.md">Performance Monitoring</a> * @param id the performance trace id * @param callerLabel the label of the caller, used to give a directed graph * of performance timings * @param label an array of service labels, should equal length of start and * end arrays * @param start an array of start times * @param end an array of end times * @param context a map of contextual metadata */ public boolean performanceTraceMessage(String id, String callerLabel, String[] label, long[] start, long[] end, Map<String, String> context) { boolean ret = false; try { List<Context> contextList = new ArrayList<Context>(); for (Entry<String, String> entry : context.entrySet()) { contextList.add(new Context(entry.getKey(), entry.getValue())); } Context[] contexts = contextList.toArray(new Context[0]); for (Transport t : transports.get(EventType.PERF)) { try { t.sendPerformanceTrace(id, callerLabel, label, start, end, contexts); } catch (TransportException te) { errorHandler.handleError("Error calling Transport.sendPerformanceTrace()", te); } } ret = true; } catch (Exception e) { errorHandler.handleError("Error calling Client.performanceTraceMessage()", e); } return ret; } /******************************** * PRIVATE API METHODS * ********************************/ private void logReal(String name, int line, int level, TraceId traceId, String message, Object[] args) { String filename = name; StringBuffer formattedMsg = new StringBuffer(); if (message == null) { return; } if (level < Level.OFF || level > Level.ALL) { errorHandler.handleError("Client.logReal() called by " + name + ":" + line + " with invalid log level: " + Integer.toString(level)); } // initialize if necessary if (this.messages == null) { this.messages = new ConcurrentHashMap<String, LogMessage>(); } // format the message formattedMsg.append(message); if (args != null) { for (int i = 0; i < args.length; ++i) { formattedMsg.append(" " + args[i].toString()); } } // figure out the name if it is null if (filename == null) { filename = ClassUtils.getCallingClass(CALLER_DEPTH); } try { if (levelIsEnabled(level, traceId)) { String key = filename + FILE_LINE_DELIMITER + Integer.toString(line); if (this.messages.containsKey(key)) { // repeated message, increment the repeat counter LogMessage msg = this.messages.get(key); if (msg != null) { msg.setRepeat(msg.getRepeat() + 1); if (msg.getRepeat() % 999 == 0) { flushLogs(); return; } this.messages.put(key, msg); } } else { // new message LogMessage msg = new LogMessage(); msg.setFilename(filename); msg.setLine(line); msg.setLevel(level); msg.setMessage(formattedMsg.toString()); msg.setRepeat(1); msg.setTraceId(traceId); this.messages.put(key, msg); } // if the trace ID is set, emit immediately if (traceId != null) { if (traceId.compareTo(TraceId.NULL_TRACE_ID) != 0) { flushLogs(); return; } } // if the immediate send level is passed, emit immediately if (level <= this.immediateSendLevel) { flushLogs(); return; } // if the message buffer is full, emit immediately if (messages.size() >= MAX_MESSAGES) { flushLogs(); return; } } } catch (Exception e) { errorHandler.handleError("Error in Client.logReal()", e); } } /** * Iterates through the transports, calling the sendLogs method for each. * Since we cannot assume transports are thread-safe, we make this method synchronized. */ private synchronized void dispatchLogs() { if (this.messages == null) { return; } try { Context[] contexts = this.contexts.values().toArray(new Context[0]); LogMessage[] messages = this.messages.values().toArray(new LogMessage[0]); for (Transport t : transports.get(EventType.LOG)) { try { t.sendLogs(programId, messages, contexts); } catch (TransportException te) { errorHandler.handleError("Error calling Transport.sendLogs()", te); } } } catch (Exception e) { errorHandler.handleError("Error calling Client.dispatchLogs()", e); } } /** * Iterates through the transports, calling the send() method for each to send * all the stats and samples. * Since we cannot assume transports are thread-safe, we make this method synchronized. */ private synchronized void dispatchStatsSamples() { if ((this.samples == null || this.samples.isEmpty()) && (this.stats == null || this.stats.isEmpty())) { return; } try { Context[] contexts = this.contexts.values().toArray(new Context[0]); StatsMessage[] statsMsgs = this.stats.values().toArray(new StatsMessage[0]); // snapshot samples map for dispatch SamplesMessage[] samplesMsgs = this.samples.values().toArray(new SamplesMessage[0]); this.samples = new ConcurrentHashMap<String, SamplesMessage>(this.samples.size()); for (Transport t : transports.get(EventType.STATS)) { try { t.send(programId, statsMsgs, samplesMsgs, contexts, this.maxNumMetrics); } catch (TransportException te) { errorHandler.handleError("Error calling Transport.sendStats()", te); } } } catch (Exception e) { errorHandler.handleError("Error calling Client.dispatchStats()", e); } } /** * emit the events */ private synchronized void dispatchContextStats() { if (this.contextStats == null || this.contextStats.isEmpty()) { return; } for (Map.Entry<ContextList, AtomicLongMap<String>> entry : contextStats.entrySet()) { List<Context> newContexts = new ArrayList<Context>(this.contexts.values()); newContexts.addAll(entry.getKey().getList()); List<StatsMessage> statsMsgs = new ArrayList<StatsMessage>(); for (Map.Entry<String, Long> stat : entry.getValue().asMap().entrySet()) { StatsMessage statsMessage = new StatsMessage(stat.getKey(), StatType.Counter); statsMessage.incrementBy(stat.getValue().intValue()); statsMsgs.add(statsMessage); } Context[] contexts = newContexts.toArray(new Context[0]); for (Transport t : transports.get(EventType.STATS)) { try { t.send(programId, statsMsgs.toArray(new StatsMessage[0]), null, contexts, this.maxNumMetrics); } catch (TransportException te) { errorHandler.handleError("Error calling Transport.sendStats()", te); } } } } public ConcurrentHashMap<ContextList, AtomicLongMap<String>> getContextStats() { return contextStats; } public ConcurrentHashMap<String, SamplesMessage> getSamples() { return samples; } }