package com.logentries.net; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.util.Random; import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.regex.Pattern; /** * Logentries Asynchronous Logger for integration with Java logging frameworks. * * a RevelOps™ service * * * VERSION: 1.2.0 * * @author Viliam Holub * @author Mark Lacomber * */ public class AsyncLogger { /* * Constants */ /** Size of the internal event queue. */ private static final int QUEUE_SIZE = 32768; /** Limit on individual log length ie. 2^16*/ public static final int LOG_LENGTH_LIMIT = 65536; /** Limit on recursion for appending long logs to queue */ private static final int RECURSION_LIMIT = 32; /** UTF-8 output character set. */ private static final Charset UTF8 = Charset.forName( "UTF-8"); /** ASCII character set used by HTTP. */ private static final Charset ASCII = Charset.forName( "US-ASCII"); /** Minimal delay between attempts to reconnect in milliseconds. */ private static final int MIN_DELAY = 100; /** Maximal delay between attempts to reconnect in milliseconds. */ private static final int MAX_DELAY = 10000; /** LE appender signature - used for debugging messages. */ private static final String LE = "LE "; /** Platform dependent line separator to check for. Supported in Java 1.6+ */ private static final String LINE_SEP = System.getProperty("line_separator", "\n"); /** Error message displayed when invalid API key is detected. */ private static final String INVALID_TOKEN = "\n\nIt appears your LOGENTRIES_TOKEN parameter in log4j.xml is incorrect!\n\n"; /** Key Value for Token Environment Variable. */ private static final String CONFIG_TOKEN = "LOGENTRIES_TOKEN"; /** Error message displayed when queue overflow occurs */ private static final String QUEUE_OVERFLOW = "\n\nLogentries Buffer Queue Overflow. Message Dropped!\n\n"; /** Identifier for this client library */ private static final String LIBRARY_ID = "###J01### - Library initialised"; /** Reg.ex. that is used to check correctness of HostName if it is defined by user */ private static final Pattern HOSTNAME_REGEX = Pattern.compile("[$/\\\"&+,:;=?#|<>_* \\[\\]]"); /* * Fields */ /** Destination Token. */ String token = ""; /** Account Key. */ String key = ""; /** Account Log Location. */ String location = ""; /** HttpPut flag. */ boolean httpPut = false; /** SSL/TLS flag. */ boolean ssl = false; /** Debug flag. */ boolean debug = false; /** Make local connection only. */ boolean local = false; /** UseDataHub flag. */ boolean useDataHub = false; /** DataHubAddr - address of the server where DataHub instance resides. */ String dataHubAddr = null; /** DataHubPort - port on which DataHub instance waits for messages */ int dataHubPort; /** LogHostName - switch that determines whether HostName should be appended to the log message */ boolean logHostName = false; /** HostName - value, that should be appended to the log message if logHostName is set to true */ String hostName = ""; /** LogID - user-defined ID string that is appended to the log message if non-empty */ String logID = ""; /** Indicator if the socket appender has been started. */ boolean started = false; /** Asynchronous socket appender. */ SocketAppender appender; /** Message queue. */ ArrayBlockingQueue<String> queue; /* * Public methods for parameters */ /** * Sets the token * * @param token */ public void setToken( String token) { this.token = token; dbg( "Setting token to " + token); } /** * Returns current token. * * @return current token */ public String getToken() { return token; } /** * Sets the HTTP PUT boolean flag. Send logs via HTTP PUT instead of default Token TCP * * @param HttpPut HttpPut flag to set */ public void setHttpPut( boolean HttpPut) { this.httpPut = HttpPut; } /** * Returns current HttpPut flag. * * @return true if HttpPut is enabled */ public boolean getHttpPut() { return this.httpPut; } /** Sets the ACCOUNT KEY value for HTTP PUT * * @param account_key */ public void setKey( String account_key) { this.key = account_key; } /** * Gets the ACCOUNT KEY value for HTTP PUT * * @return key */ public String getKey() { return this.key; } /** * Gets the LOCATION value for HTTP PUT * * @param log_location */ public void setLocation( String log_location) { this.location = log_location; } /** * Gets the LOCATION value for HTTP PUT * * @return location */ public String getLocation() { return location; } /** * Sets the SSL boolean flag * * @param ssl */ public void setSsl( boolean ssl) { this.ssl = ssl; } /** * Gets the SSL boolean flag * * @return ssl */ public boolean getSsl() { return this.ssl; } /** * Sets the debug flag. Appender in debug mode will print error messages on * error console. * * @param debug debug flag to set */ public void setDebug( boolean debug) { this.debug = debug; dbg( "Setting debug to " + debug); } /** * Returns current debug flag. * * @return true if debugging is enabled */ public boolean getDebug() { return debug; } /** * Sets useDataHub flag. If set to "true" then messages will be sent to a DataHub instance. * * @param isUsingDataHub */ public void setUseDataHub(boolean isUsingDataHub) { this.useDataHub = isUsingDataHub; } /** * Gets value of useDataHub flag. * * @return true if a DataHub instance is used as receiver for log messages. */ public boolean getUseDataHub() { return this.useDataHub; } /** * Sets DataHub instance address. * * @param dataHubAddr */ public void setDataHubAddr(String dataHubAddr) { this.dataHubAddr = dataHubAddr; } /** * Gets DataHub instance address. * * @return DataHub address represented as String */ public String getDataHubAddr() { return this.dataHubAddr; } /** * Sets port on which DataHub waits for log messages. * * @param dataHubPort */ public void setDataHubPort(int dataHubPort) { this.dataHubPort = dataHubPort; } /** * Gets port on which DataHub waits for log messages. * * @return port number. */ public int getDataHubPort() { return this.dataHubPort; } /** * Sets value of the switch that determines whether to send HostName alongside with the log message * * @param logHostName */ public void setLogHostName(boolean logHostName) { this.logHostName = logHostName; } /** * Gets value of the switch that determines whether to send HostName alongside with the log message * * @return logHostName switch value */ public boolean getLogHostName() { return this.logHostName; } /** * Sets the HostName from configuration * * @param hostName */ public void setHostName(String hostName) { this.hostName = hostName; } /** * Gets HostName parameter * * @return Host name field value */ public String getHostName() { return this.hostName; } /** * Sets LogID parameter from config * * @param logID */ public void setLogID(String logID) { this.logID = logID; } /** * Gets LogID parameter * * @return logID field value */ public String getLogID() { return this.logID; } /** * Initializes asynchronous logging. * * @param local make local connection to API server for testing */ AsyncLogger( boolean local) { this.local = local; queue = new ArrayBlockingQueue<String>(QUEUE_SIZE); // Fill the queue with an identifier message for first entry sent to server queue.offer(LIBRARY_ID); appender = new SocketAppender(); } /** * Initializes asynchronous logging. */ public AsyncLogger() { this( false); } /** * Checks that the UUID is valid */ boolean checkValidUUID( String uuid){ if("".equals(uuid)) return false; try { UUID u = UUID.fromString(uuid); }catch(IllegalArgumentException e){ return false; } return true; } /** * Try and retrieve environment variable for given key, return empty string if not found */ String getEnvVar( String key) { String envVal = System.getenv(key); return envVal != null ? envVal : ""; } /** * Checks that key and location are set. */ boolean checkCredentials() { if(!httpPut) { if (token.equals(CONFIG_TOKEN) || token.equals("")) { //Check if set in an environment variable, used with PaaS providers String envToken = getEnvVar( CONFIG_TOKEN); if (envToken == ""){ dbg(INVALID_TOKEN); return false; } this.setToken(envToken); } return checkValidUUID(this.getToken()); }else{ if ( !checkValidUUID(this.getKey()) || this.getLocation().equals("")) return false; return true; } } /** * Checks whether given host name is valid (e.g. does not contain any prohibited characters) * @param hostName - string containing host name */ boolean checkIfHostNameValid(String hostName) { return !HOSTNAME_REGEX.matcher(hostName).find(); } /** * Adds the data to internal queue to be sent over the network. * * It does not block. If the queue is full, it removes latest event first to * make space. * * @param line line to append */ public void addLineToQueue(String line) { addLineToQueue(line, RECURSION_LIMIT); } private void addLineToQueue (String line, int limit) { if (limit == 0) { dbg("Message longer than " + RECURSION_LIMIT * LOG_LENGTH_LIMIT); return; } //// Check credentials only if logs are sent to LE directly. // Check that we have all parameters set and socket appender running. // If DataHub mode is used then credentials check is ignored. if (!this.started && (useDataHub || this.checkCredentials())) { dbg("Starting Logentries asynchronous socket appender"); appender.start(); started = true; } dbg("Queueing " + line); // If individual string is too long add it to the queue recursively as sub-strings if (line.length() > LOG_LENGTH_LIMIT) { if (!queue.offer(line.substring(0, LOG_LENGTH_LIMIT))) { queue.poll(); if (!queue.offer(line.substring(0, LOG_LENGTH_LIMIT))) dbg(QUEUE_OVERFLOW); } addLineToQueue(line.substring(LOG_LENGTH_LIMIT, line.length()), limit - 1); } else { // Try to append data to queue if (!queue.offer(line)) { queue.poll(); if (!queue.offer(line)) dbg(QUEUE_OVERFLOW); } } } /** * Closes all connections to Logentries. */ public void close() { appender.interrupt(); started = false; dbg( "Closing Logentries asynchronous socket appender"); } /** * Prints the message given. Used for internal debugging. * * @param msg message to display */ void dbg(String msg) { if (debug ) { if (!msg.endsWith(LINE_SEP)) { System.err.println(LE + msg); } else { System.err.print(LE + msg); } } } /** * Asynchronous over the socket appender. * * @author Viliam Holub * */ class SocketAppender extends Thread { /** Random number generator for delays between reconnection attempts. */ final Random random = new Random(); /** Logentries Client for connecting to Logentries via HTTP or TCP. */ LogentriesClient le_client; /** * Initializes the socket appender. */ SocketAppender() { super( "Logentries Socket appender"); // Don't block shut down setDaemon( true); } /** * Opens connection to Logentries. * * @throws IOException */ void openConnection() throws IOException { try{ if(this.le_client == null){ this.le_client = new LogentriesClient(httpPut, ssl, useDataHub, dataHubAddr, dataHubPort); } this.le_client.connect(); if(httpPut){ final String f = "PUT /%s/hosts/%s/?realtime=1 HTTP/1.1\r\n\r\n"; final String header = String.format( f, key, location); byte[] temp = header.getBytes( ASCII); this.le_client.write( temp, 0, temp.length); } }catch(Exception e){ } } /** * Tries to opens connection to Logentries until it succeeds. * * @throws InterruptedException */ void reopenConnection() throws InterruptedException { // Close the previous connection closeConnection(); // Try to open the connection until we get through int root_delay = MIN_DELAY; while (true) { try { openConnection(); // Success, leave return; } catch (IOException e) { // Get information if in debug mode if (debug) { dbg( "Unable to connect to Logentries"); e.printStackTrace(); } } // Wait between connection attempts root_delay *= 2; if (root_delay > MAX_DELAY) root_delay = MAX_DELAY; int wait_for = root_delay + random.nextInt( root_delay); dbg( "Waiting for " + wait_for + "ms"); Thread.sleep( wait_for); } } /** * Closes the connection. Ignores errors. */ void closeConnection() { if (this.le_client != null) this.le_client.close(); } /** * Builds the prefix message for the StringBuilder. */ private void buildPrefixMessage(StringBuilder sb){ if(!logID.isEmpty()) { sb.append(logID).append(" "); // Append LogID and separator between logID and the rest part of the message. } if(logHostName) { if(hostName.isEmpty()) { dbg("Host name is not defined by user - trying to obtain it from the environment."); try { hostName = InetAddress.getLocalHost().getHostName(); sb.append("HostName=").append(hostName).append(" "); } catch (UnknownHostException e) { // We cannot resolve local host name - so won't use it at all. dbg("Failed to get host name automatically; Host name will not be used in prefix."); } } else { if(!checkIfHostNameValid(hostName)) { // User-defined HostName is invalid - e.g. with prohibited characters, // so we'll not use it. dbg("There are some prohibited characters found in the host name defined in the config; Host name will not be used in prefix."); } else { sb.append("HostName=").append(hostName).append(" "); } } } } /** * Initializes the connection and starts to log. * */ @Override public void run() { try { // Open connection reopenConnection(); String logMessagePrefix = ""; StringBuilder sb = new StringBuilder(logMessagePrefix); buildPrefixMessage(sb); boolean logPrefixEmpty; if(!(logPrefixEmpty = sb.toString().isEmpty())) { logMessagePrefix = sb.toString(); } // Use StringBuilder here because if use just overloaded // + operator it may give much more work for allocator and GC. StringBuilder finalDataBuilder = new StringBuilder(""); // Send data in queue while (true) { // Take data from queue String data = queue.take(); // Replace platform-independent carriage return with unicode line separator character to format multi-line events nicely in Logentries UI data = data.replace(LINE_SEP, "\u2028"); finalDataBuilder.setLength(0); // Clear the buffer to be re-used - it may be faster than re-allocating space for new String instances. // If we're neither sending to DataHub nor using HTTP PUT // then append the token to the start of the message. if(!httpPut && !useDataHub) { finalDataBuilder.append(token); } // If message prefix (LogID + HostName) is not empty // then add it to the message. if(!logPrefixEmpty) { finalDataBuilder.append(logMessagePrefix); } // Append the event data finalDataBuilder.append(data).append('\n'); // Get bytes of final event byte[] finalLine = finalDataBuilder.toString().getBytes(UTF8); // Send data, reconnect if needed while (true) { try { this.le_client.write( finalLine, 0, finalLine.length); } catch (IOException e) { // Reopen the lost connection reopenConnection(); continue; } break; } } } catch (InterruptedException e) { // We got interrupted, stop dbg( "Asynchronous socket writer interrupted"); dbg("Queue had "+queue.size()+" lines left in it"); } closeConnection(); } } }