You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Radarr/src/LogentriesCore/AsyncLogger.cs

648 lines
21 KiB

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)
{
// 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
}
}