648 lines
21 KiB
C#
648 lines
21 KiB
C#
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<BlockingCollection<string>> _allQueues = new ConcurrentBag<BlockingCollection<string>>();
|
|
|
|
/// <summary>
|
|
/// Determines if the queue is empty after waiting the specified waitTime.
|
|
/// Returns true or false if the underlying queues are empty.
|
|
/// </summary>
|
|
/// <param name="waitTime">The length of time the method should block before giving up waiting for it to empty.</param>
|
|
/// <returns>True if the queue is empty, false if there are still items waiting to be written.</returns>
|
|
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<string>(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<string> 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 ex)
|
|
{
|
|
// 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
|
|
}
|
|
} |