using System; using System.Collections.Concurrent; using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; using System.Security; using System.Text; using System.Text.RegularExpressions; using System.Threading; namespace LogentriesCore { public class AsyncLogger { #region Constants // Current version number. protected const String Version = "2.6.0"; // Size of the internal event queue. protected const int QueueSize = 32768; // Minimal delay between attempts to reconnect in milliseconds. protected const int MinDelay = 100; // Maximal delay between attempts to reconnect in milliseconds. protected const int MaxDelay = 10000; // Appender signature - used for debugging messages. protected const String LeSignature = "LE: "; // Legacy Logentries configuration names. protected const String LegacyConfigTokenName = "LOGENTRIES_TOKEN"; protected const String LegacyConfigAccountKeyName = "LOGENTRIES_ACCOUNT_KEY"; protected const String LegacyConfigLocationName = "LOGENTRIES_LOCATION"; // New Logentries configuration names. protected const String ConfigTokenName = "Logentries.Token"; protected const String ConfigAccountKeyName = "Logentries.AccountKey"; protected const String ConfigLocationName = "Logentries.Location"; // Error message displayed when invalid token is detected. protected const String InvalidTokenMessage = "\n\nIt appears your LOGENTRIES_TOKEN value is invalid or missing.\n\n"; // Error message displayed when invalid account_key or location parameters are detected. protected const String InvalidHttpPutCredentialsMessage = "\n\nIt appears your LOGENTRIES_ACCOUNT_KEY or LOGENTRIES_LOCATION values are invalid or missing.\n\n"; // Error message deisplayed when queue overflow occurs. protected const String QueueOverflowMessage = "\n\nLogentries buffer queue overflow. Message dropped.\n\n"; // Newline char to trim from message for formatting. protected static char[] TrimChars = { '\r', '\n' }; /** Non-Unix and Unix Newline */ protected static string[] posix_newline = { "\r\n", "\n" }; /** Unicode line separator character */ protected static string line_separator = "\u2028"; // Restricted symbols that should not appear in host name. // See http://support.microsoft.com/kb/228275/en-us for details. private static Regex ForbiddenHostNameChars = new Regex(@"[/\\\[\]\""\:\;\|\<\>\+\=\,\?\* _]{1,}", RegexOptions.Compiled); /** Regex used to validate GUID in .NET3.5 */ private static Regex isGuid = new Regex(@"^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$", RegexOptions.Compiled); #endregion #region Singletons // UTF-8 output character set. protected static readonly UTF8Encoding UTF8 = new UTF8Encoding(); // ASCII character set used by HTTP. protected static readonly ASCIIEncoding ASCII = new ASCIIEncoding(); //static list of all the queues the le appender might be managing. private static ConcurrentBag> _allQueues = new ConcurrentBag>(); /// /// Determines if the queue is empty after waiting the specified waitTime. /// Returns true or false if the underlying queues are empty. /// /// The length of time the method should block before giving up waiting for it to empty. /// True if the queue is empty, false if there are still items waiting to be written. public static bool AreAllQueuesEmpty(TimeSpan waitTime) { var start = DateTime.UtcNow; var then = DateTime.UtcNow; while (start.Add(waitTime) > then) { if (_allQueues.All(x => x.Count == 0)) return true; Thread.Sleep(100); then = DateTime.UtcNow; } return _allQueues.All(x => x.Count == 0); } #endregion public AsyncLogger() { Queue = new BlockingCollection(QueueSize); _allQueues.Add(Queue); WorkerThread = new Thread(Run); WorkerThread.Name = "Logentries Log4net Appender"; WorkerThread.IsBackground = true; } #region Configuration properties private String m_Token = ""; private String m_AccountKey = ""; private String m_Location = ""; private bool m_ImmediateFlush = false; private bool m_Debug = false; private bool m_UseHttpPut = false; private bool m_UseSsl = false; // Properties for defining location of DataHub instance if one is used. private bool m_UseDataHub = false; // By default Logentries service is used instead of DataHub instance. private String m_DataHubAddr = ""; private int m_DataHubPort = 0; // Properties to define host name of user's machine and define user-specified log ID. private bool m_UseHostName = false; // Defines whether to prefix log message with HostName or not. private String m_HostName = ""; // User-defined or auto-defined host name (if not set in config. file) private String m_LogID = ""; // User-defined log ID to be prefixed to the log message. // Sets DataHub usage flag. public void setIsUsingDataHub(bool useDataHub) { m_UseDataHub = useDataHub; } public bool getIsUsingDataHab() { return m_UseDataHub; } // Sets DataHub instance address. public void setDataHubAddr(String dataHubAddr) { m_DataHubAddr = dataHubAddr; } public String getDataHubAddr() { return m_DataHubAddr; } // Sets the port on which DataHub instance is waiting for log messages. public void setDataHubPort(int port) { m_DataHubPort = port; } public int getDataHubPort() { return m_DataHubPort; } public void setToken(String token) { m_Token = token; } public String getToken() { return m_Token; } public void setAccountKey(String accountKey) { m_AccountKey = accountKey; } public string getAccountKey() { return m_AccountKey; } public void setLocation(String location) { m_Location = location; } public String getLocation() { return m_Location; } public void setImmediateFlush(bool immediateFlush) { m_ImmediateFlush = immediateFlush; } public bool getImmediateFlush() { return m_ImmediateFlush; } public void setDebug(bool debug) { m_Debug = debug; } public bool getDebug() { return m_Debug; } public void setUseHttpPut(bool useHttpPut) { m_UseHttpPut = useHttpPut; } public bool getUseHttpPut() { return m_UseHttpPut; } public void setUseSsl(bool useSsl) { m_UseSsl = useSsl; } public bool getUseSsl() { return m_UseSsl; } public void setUseHostName(bool useHostName) { m_UseHostName = useHostName; } public bool getUseHostName() { return m_UseHostName; } public void setHostName(String hostName) { m_HostName = hostName; } public String getHostName() { return m_HostName; } public void setLogID(String logID) { m_LogID = logID; } public String getLogID() { return m_LogID; } #endregion protected readonly BlockingCollection Queue; protected readonly Thread WorkerThread; protected readonly Random Random = new Random(); private LeClient LeClient = null; protected bool IsRunning = false; #region Protected methods protected virtual void Run() { try { // Open connection. ReopenConnection(); string logMessagePrefix = String.Empty; if (m_UseHostName) { // If LogHostName is set to "true", but HostName is not defined - // try to get host name from Environment. if (m_HostName == String.Empty) { try { WriteDebugMessages("HostName parameter is not defined - trying to get it from System.Environment.MachineName"); m_HostName = "HostName=" + System.Environment.MachineName + " "; } catch (InvalidOperationException) { // Cannot get host name automatically, so assume that HostName is not used // and log message is sent without it. m_UseHostName = false; WriteDebugMessages("Failed to get HostName parameter using System.Environment.MachineName. Log messages will not be prefixed by HostName"); } } else { if (!CheckIfHostNameValid(m_HostName)) { // If user-defined host name is incorrect - we cannot use it // and log message is sent without it. m_UseHostName = false; WriteDebugMessages("HostName parameter contains prohibited characters. Log messages will not be prefixed by HostName"); } else { m_HostName = "HostName=" + m_HostName + " "; } } } if (m_LogID != String.Empty) { logMessagePrefix = m_LogID + " "; } if (m_UseHostName) { logMessagePrefix += m_HostName; } // Flag that is set if logMessagePrefix is empty. bool isPrefixEmpty = (logMessagePrefix == String.Empty); // Send data in queue. while (true) { // Take data from queue. var line = Queue.Take(); // Replace newline chars with line separator to format multi-line events nicely. foreach (String newline in posix_newline) { line = line.Replace(newline, line_separator); } // If m_UseDataHub == true (logs are sent to DataHub instance) then m_Token is not // appended to the message. string finalLine = ((!m_UseHttpPut && !m_UseDataHub) ? this.m_Token + line : line) + '\n'; // Add prefixes: LogID and HostName if they are defined. if (!isPrefixEmpty) { finalLine = logMessagePrefix + finalLine; } byte[] data = UTF8.GetBytes(finalLine); // Send data, reconnect if needed. while (true) { try { this.LeClient.Write(data, 0, data.Length); if (m_ImmediateFlush) this.LeClient.Flush(); } catch (IOException) { // Reopen the lost connection. ReopenConnection(); continue; } break; } } } catch (ThreadInterruptedException ex) { WriteDebugMessages("Logentries asynchronous socket client was interrupted.", ex); } } protected virtual void OpenConnection() { try { if (LeClient == null) { // Create LeClient instance providing all needed parameters. If DataHub-related properties // have not been overridden by log4net or NLog configurators, then DataHub is not used, // because m_UseDataHub == false by default. LeClient = new LeClient(m_UseHttpPut, m_UseSsl, m_UseDataHub, m_DataHubAddr, m_DataHubPort); } LeClient.Connect(); if (m_UseHttpPut) { var header = String.Format("PUT /{0}/hosts/{1}/?realtime=1 HTTP/1.1\r\n\r\n", m_AccountKey, m_Location); LeClient.Write(ASCII.GetBytes(header), 0, header.Length); } } catch (Exception ex) { throw new IOException("An error occurred while opening the connection.", ex); } } protected virtual void ReopenConnection() { CloseConnection(); var rootDelay = MinDelay; while (true) { try { OpenConnection(); return; } catch (Exception ex) { if (m_Debug) { WriteDebugMessages("Unable to connect to Logentries API.", ex); } } rootDelay *= 2; if (rootDelay > MaxDelay) rootDelay = MaxDelay; var waitFor = rootDelay + Random.Next(rootDelay); try { Thread.Sleep(waitFor); } catch { throw new ThreadInterruptedException(); } } } protected virtual void CloseConnection() { if (LeClient != null) LeClient.Close(); } public static bool IsNullOrWhiteSpace(String value) { if (value == null) return true; for (int i = 0; i < value.Length; i++) { if (!Char.IsWhiteSpace(value[i])) return false; } return true; } private string retrieveSetting(String name) { string value; value = ConfigurationManager.AppSettings[name]; if (IsNullOrWhiteSpace(value)) { try { value = Environment.GetEnvironmentVariable(name); } catch (SecurityException) { } } return value; } /* * Use CloudConfigurationManager with .NET4.0 and fallback to System.Configuration for previous frameworks. * * NOTE: This is not entirely clear with regards to the above comment, but this block of code uses a compiler directive NET4_0 * which is not set by default anywhere, so most uses of this code will default back to the "pre-.Net4.0" code branch, even * if you are using .Net4.0 or .Net4.5. * * The second issue is that there are two appsetting keys for each setting - the "legacy" key, such as "LOGENTRIES_TOKEN" * and the "non-legacy" key, such as "Logentries.Token". Again, I'm not sure of the reasons behind this, so the code below checks * both the legacy and non-legacy keys, defaulting to the legacy keys if they are found. * * It probably should be investigated whether the fallback to ConfigurationManager is needed at all, as CloudConfigurationManager * will retrieve settings from appSettings in a non-Azure environment. */ protected virtual bool LoadCredentials() { if (!m_UseHttpPut) { if (GetIsValidGuid(m_Token)) return true; var configToken = retrieveSetting(LegacyConfigTokenName) ?? retrieveSetting(ConfigTokenName); if (!String.IsNullOrEmpty(configToken) && GetIsValidGuid(configToken)) { m_Token = configToken; return true; } WriteDebugMessages(InvalidTokenMessage); return false; } if (m_AccountKey != "" && GetIsValidGuid(m_AccountKey) && m_Location != "") return true; var configAccountKey = ConfigurationManager.AppSettings[LegacyConfigAccountKeyName] ?? ConfigurationManager.AppSettings[ConfigAccountKeyName]; if (!String.IsNullOrEmpty(configAccountKey) && GetIsValidGuid(configAccountKey)) { m_AccountKey = configAccountKey; var configLocation = ConfigurationManager.AppSettings[LegacyConfigLocationName] ?? ConfigurationManager.AppSettings[ConfigLocationName]; if (!String.IsNullOrEmpty(configLocation)) { m_Location = configLocation; return true; } } WriteDebugMessages(InvalidHttpPutCredentialsMessage); return false; } private bool CheckIfHostNameValid(String hostName) { return !ForbiddenHostNameChars.IsMatch(hostName); // Returns false if reg.ex. matches any of forbidden chars. } static bool IsGuid(string candidate, out Guid output) { bool isValid = false; output = Guid.Empty; if (isGuid.IsMatch(candidate)) { output = new Guid(candidate); isValid = true; } return isValid; } protected virtual bool GetIsValidGuid(string guidString) { if (String.IsNullOrEmpty(guidString)) return false; System.Guid newGuid = System.Guid.NewGuid(); return IsGuid(guidString, out newGuid); } protected virtual void WriteDebugMessages(string message, Exception ex) { if (!m_Debug) return; message = LeSignature + message; string[] messages = { message, ex.ToString() }; foreach (var msg in messages) { // Use below line instead when compiling with log4net1.2.10. //LogLog.Debug(msg); //LogLog.Debug(typeof(LogentriesAppender), msg); Debug.WriteLine(message); } } protected virtual void WriteDebugMessages(string message) { if (!m_Debug) return; message = LeSignature + message; // Use below line instead when compiling with log4net1.2.10. //LogLog.Debug(message); //LogLog.Debug(typeof(LogentriesAppender), message); Debug.WriteLine(message); } #endregion #region publicMethods public virtual void AddLine(string line) { if (!IsRunning) { // We need to load user credentials only // if the configuration does not state that DataHub is used; // credentials needed only if logs are sent to LE service directly. bool credentialsLoaded = false; if(!m_UseDataHub) { credentialsLoaded = LoadCredentials(); } // If in DataHub mode credentials are ignored. if (credentialsLoaded || m_UseDataHub) { WriteDebugMessages("Starting Logentries asynchronous socket client."); WorkerThread.Start(); IsRunning = true; } } WriteDebugMessages("Queueing: " + line); String trimmedEvent = line.TrimEnd(TrimChars); // Try to append data to queue. if (!Queue.TryAdd(trimmedEvent)) { Queue.Take(); if (!Queue.TryAdd(trimmedEvent)) WriteDebugMessages(QueueOverflowMessage); } } public void interruptWorker() { WorkerThread.Interrupt(); } #endregion } }