/* * Copyright 2012-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DocumentModel; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; using Amazon.Runtime.Internal.Util; using ThirdParty.Json.LitJson; using Timer = System.Timers.Timer; namespace Amazon.TraceListener { /// /// /// DynamoDBTraceListener is a custom TraceListener that logs events to a DynamoDB table. /// The listener can be configured through the application's .config file or by instantiating /// an instance of DynamoDBTraceListener and setting the Configuration property on the instance. /// /// /// /// The target table must have a string hash key and a string range key. /// If the table does not exist the listener will create it during initialization with /// default read and write units set to 1 and 10, respectively, unless configured otherwise. /// /// /// /// While DynamoDBTraceListener is running, it will write temporary log files into current /// directory. These log files will be deleted once the data is pushed to DynamoDB. /// The logs are pushed to DynamoDB under the following conditions: /// 1. Flush is called on the DynamoDBTraceListener /// 2. Close is called on the DynamoDBTraceListener /// 3. WritePeriod has elapsed since last write /// /// (If the listener is used with the SDK clients, Flush is invoked when the client is disposed.) /// /// If the application exits and there are still log files (in the event Flush is not invoked or /// the application terminates unexpectedly), these log files will be pushed to DynamoDB on the /// next execution of the application. /// /// Log files can also be flushed manually by creating an instance of DynamoDBTraceListener and /// using the FlushLog method on a particular log file. /// /// /// /// Example of an app.config entry setting up the listener with all possible configurations specified: /// /// <system.diagnostics> /// <trace> /// <listeners> /// <add name="dynamo" type="Amazon.TraceListener.DynamoDBTraceListener, AWS.TraceListener" /// AWSProfileName="default" /// AWSProfilesLocation=".aws/credentials" /// Region="us-west-2" /// Table="Logs" /// CreateIfNotExist="true" /// ReadCapacityUnits="1" /// WriteCapacityUnits="10" /// HashKey="Origin" /// RangeKey="Timestamp" /// MaxLength="10000" /// ExcludeAttributes="Callstack, Host" /// HashKeyFormat="{Host}-{EventType}-{ProcessId}" /// RangeKeyFormat="{Time}" /// WritePeriodMs="60000" /// LogFilesDir="C:\Logs" /// /> /// </listeners> /// </trace> /// </system.diagnostics> /// /// /// public class DynamoDBTraceListener : System.Diagnostics.TraceListener { #region Private fields private object generalLock = new object(); private Timer writeTimer = null; private TextWriter writer = null; private DateTime lastTimestamp = DateTime.MinValue; private static string logFileNameFormat = string.Format("{0}.{1}.log", typeof(DynamoDBTraceListener).FullName, "{0}"); private static string eventLogsFileName = "events.log"; private static string eventLogsFormat = "{0}.{1} {2} > {3}"; private static string logFileSearchPattern = string.Format(logFileNameFormat, "*"); private static string tempLogFileName = string.Concat(Guid.NewGuid().ToString(), ".", string.Format(logFileNameFormat, "temp")); private const int bufferSize = 0x1000; // 4 KB private static bool canWriteToEventLog; private static string eventLogSource = typeof(DynamoDBTraceListener).Name; private static DynamoDBEntryConversion conversionSchema = DynamoDBEntryConversion.V1; private string _currentLogFile = null; private string CurrentLogFile { get { if (_currentLogFile == null) { _currentLogFile = GetNewLogFilePath(); } return _currentLogFile; } } private string _eventLogsFile = null; private string GetEventLogsFileLocation() { if (_eventLogsFile == null) { if (!string.IsNullOrEmpty(_logFileDirectory)) _eventLogsFile = Path.Combine(_logFileDirectory, eventLogsFileName); } return _eventLogsFile; } private string _logFileDirectory = null; private string LogFileDirectory { get { if (_logFileDirectory == null) { // Try the log directories in order string[] directoriesToTest = { Configuration.LogFilesDirectory, // user-specified Directory.GetCurrentDirectory(), // current Path.Combine(Path.GetTempPath(), "DDBTLLogs") // Windows Temp }; foreach (var path in directoriesToTest) { if (string.IsNullOrEmpty(path)) continue; string directory = Path.GetFullPath(path); try { if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); string testPath = Path.Combine(directory, tempLogFileName); // Test write File.WriteAllText(testPath, testPath); // If write succeeds, delete file and use path if (File.Exists(testPath)) { File.Delete(testPath); _logFileDirectory = directory; break; } } catch { } } if (_logFileDirectory == null) { // If no directory has been set, disable the listener. DisableListener("Could not determine log file directory"); } else WriteLogMessage("DynamoDBTraceListener will store temporary log files under " + _logFileDirectory, EventLogEntryType.Information); } return _logFileDirectory; } } private AmazonDynamoDBClient _client = null; private AmazonDynamoDBClient Client { get { if (_client == null) { try { AmazonDynamoDBConfig config = new AmazonDynamoDBConfig { DisableLogging = true }; if (Configuration.Region != null) config.RegionEndpoint = Configuration.Region; else if (!string.IsNullOrEmpty(Configuration.ServiceURL)) config.ServiceURL = Configuration.ServiceURL; AWSCredentials credentials = null; if (!string.IsNullOrEmpty(Configuration.AWSAccessKey) && !string.IsNullOrEmpty(Configuration.AWSSecretKey)) credentials = new BasicAWSCredentials(Configuration.AWSAccessKey, Configuration.AWSSecretKey); else if (!string.IsNullOrEmpty(Configuration.AWSProfileName)) { if (string.IsNullOrEmpty(Configuration.AWSProfilesLocation)) credentials = new StoredProfileAWSCredentials(Configuration.AWSProfileName); else credentials = new StoredProfileAWSCredentials(Configuration.AWSProfileName, Configuration.AWSProfilesLocation); } else if (Configuration.AWSCredentials != null) credentials = Configuration.AWSCredentials; if (credentials != null) _client = new AmazonDynamoDBClient(credentials, config); else _client = new AmazonDynamoDBClient(config); } catch (Exception e) { DisableListener("Could not construct AmazonDynamoDBClient: " + e); } } return _client; } } private bool _isTableActive = false; private bool IsTableActive { get { if (!_isTableActive) { lock (generalLock) { if (!_isTableActive) { try { DescribeTableRequest describeRequest = new DescribeTableRequest { TableName = Configuration.TableName }; DescribeTableResponse descResponse = Client.DescribeTable(describeRequest); string tableStatus = descResponse.Table.TableStatus; if (string.Equals(tableStatus, activeStatus, StringComparison.OrdinalIgnoreCase)) _isTableActive = true; } catch (Exception e) { DisableListener(string.Format("Table {0} could not be described: {1}", Configuration.TableName, e)); } } } } return _isTableActive; } } private Table _table = null; private Table Table { get { if (_table == null) { lock (generalLock) { if (_table == null) { // Get/create table if (!Table.TryLoadTable(Client, Configuration.TableName, out _table)) { if (Configuration.CreateTableIfNotExist) _table = CreateTable(); else { DisableListener(string.Format("Table {0} was not found to be used to log, and autocreate is turned off.", Configuration.TableName)); return null; } } // Validate table if (_table == null) DisableListener(string.Format("Table {0} could not be found or created", Configuration.TableName)); else if (_table.HashKeys == null || _table.HashKeys.Count != 1) DisableListener(string.Format("Table {0} was found, but does not contain a single hash key", Configuration.TableName)); else if (_table.RangeKeys == null || _table.RangeKeys.Count != 1) DisableListener(string.Format("Table {0} was found, but does not contain a single range key", Configuration.TableName)); } } } return _table; } } private TraceEventCache TraceEventCache { get { return new TraceEventCache(); } } #endregion #region Properties/configuration private static Regex variableRegex = new Regex(@"\{(\w*?)\}", RegexOptions.Compiled); private static TimeSpan oneMillisecond = TimeSpan.FromMilliseconds(1); private static int minWritePeriodMs = (int)TimeSpan.FromSeconds(10).TotalMilliseconds; private static Configs defaultConfigs = new Configs(); private static string host = System.Environment.MachineName; private const string activeStatus = "Active"; private const string ellipsis = "..."; private string ATTRIBUTE_ORIGIN { get { return Table.HashKeys[0]; } } private string ATTRIBUTE_TIMESTAMP { get { return Table.RangeKeys[0]; } } private const string ATTRIBUTE_MESSAGE = "Message"; private const string ATTRIBUTE_HOST = "Host"; private const string ATTRIBUTE_TIME = "Time"; private const string ATTRIBUTE_SOURCE = "Source"; private const string ATTRIBUTE_CALLSTACK = "Callstack"; private const string ATTRIBUTE_PROCESSID = "ProcessId"; private const string ATTRIBUTE_THREADID = "ThreadId"; private const string ATTRIBUTE_EVENTID = "EventId"; private const string ATTRIBUTE_EVENTTYPE = "EventType"; private const string CONFIG_ACCESSKEY = "AWSAccessKey"; private const string CONFIG_SECRETKEY = "AWSSecretKey"; private const string CONFIG_PROFILENAME = "AWSProfileName"; private const string CONFIG_PROFILESLOCATION = "AWSProfilesLocation"; private const string CONFIG_REGION = "Region"; private const string CONFIG_SERVICE_URL = "ServiceURL"; private const string CONFIG_TABLE = "Table"; private const string CONFIG_CREATE_TABLE_IF_NOT_EXIST = "CreateIfNotExist"; private const string CONFIG_READ_UNITS = "ReadCapacityUnits"; private const string CONFIG_WRITE_UNITS = "WriteCapacityUnits"; private const string CONFIG_HASHKEY = "HashKey"; private const string CONFIG_RANGEKEY = "RangeKey"; private const string CONFIG_MAXLENGTH = "MaxLength"; private const string CONFIG_EXCLUDEATTRIBUTES = "ExcludeAttributes"; private const string CONFIG_HASHKEYFORMAT = "HashKeyFormat"; private const string CONFIG_RANGEKEYFORMAT = "RangeKeyFormat"; private const string CONFIG_WRITEPERIODMS = "WritePeriodMs"; private const string CONFIG_LOGFILESDIR = "LogFilesDir"; private Configs _configuration = null; /// /// Current configuration. /// public Configs Configuration { get { if (_configuration == null) { lock (generalLock) { if (_configuration == null) { _configuration = GetConfigFromAttributes(); } } } return _configuration; } set { lock (generalLock) { _configuration = value; } } } /// /// Whether the DynamoDBTraceListener is enabled. /// Read-only field that is set to false if the listener is disabled. /// The listener can be disabled if initialization has failed (in which case an error will be /// logged to the event log) or the listener was closed. /// public bool IsEnabled { get; private set; } #endregion #region Private methods // Get a new time-stamped log file path. private string GetNewLogFilePath() { string logFileName = string.Format(logFileNameFormat, DateTime.Now.ToFileTime()); string logFilePath = Path.Combine(LogFileDirectory, logFileName); return logFilePath; } // Disposes of and nulls out the writer private void DisposeWriter() { if (writer != null) { try { writer.Flush(); writer.Close(); } catch { } writer = null; } } // Create logs table and return Table object. Table won't be active yet. private Table CreateTable() { try { Client.CreateTable(new CreateTableRequest { TableName = Configuration.TableName, KeySchema = new List { new KeySchemaElement { AttributeName = defaultConfigs.HashKey, KeyType = "HASH" }, new KeySchemaElement { AttributeName = defaultConfigs.RangeKey, KeyType = "RANGE" } }, AttributeDefinitions = new List { new AttributeDefinition { AttributeName = defaultConfigs.HashKey, AttributeType = "S" }, new AttributeDefinition { AttributeName = defaultConfigs.RangeKey, AttributeType = "S" } }, ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = Configuration.ReadUnits, WriteCapacityUnits = Configuration.WriteUnits } }); } catch (Exception e) { WriteLogMessage(string.Format("Error while creating table {0}: {1}", Configuration.TableName, e.ToString()), EventLogEntryType.Error); return null; } Table table = Table.LoadTable(Client, Configuration.TableName, conversionSchema); return table; } // Expands environment and attribute variables in string private string ExpandVariables(string value, Document doc) { string result = value; result = Environment.ExpandEnvironmentVariables(result); result = variableRegex.Replace(result, match => { string key = match.Groups[1].Captures[0].Value; if (doc.Contains(key)) return doc[key].AsString(); else return match.Captures[0].Value; }); return result; } // Composes a Document and pushes to background thread to log whenever private void Log(TraceEventCache eventCache, string source, TraceEventType eventType, int eventId, params object[] data) { if (!IsEnabled) return; Document doc = new Document(); // Populate event data doc[ATTRIBUTE_CALLSTACK] = LimitLength(eventCache.Callstack); doc[ATTRIBUTE_PROCESSID] = eventCache.ProcessId; doc[ATTRIBUTE_THREADID] = eventCache.ThreadId; doc[ATTRIBUTE_HOST] = LimitLength(host); doc[ATTRIBUTE_SOURCE] = LimitLength(source); doc[ATTRIBUTE_EVENTTYPE] = eventType.ToString(); doc[ATTRIBUTE_EVENTID] = eventId; doc[ATTRIBUTE_TIME] = GetCurrentTimestamp(); // Set the message if (data != null && data.Length > 0) { string message = ComposeMessage(data); doc[ATTRIBUTE_MESSAGE] = LimitLength(message); } doc = doc.ForceConversion(conversionSchema); // Set hash/range keys, possibly from event data doc[ATTRIBUTE_ORIGIN] = ExpandVariables(Configuration.HashKeyFormat, doc); doc[ATTRIBUTE_TIMESTAMP] = ExpandVariables(Configuration.RangeKeyFormat, doc); // Remove attributes that should be excluded if (Configuration.ExcludeAttributes != null) { foreach (string exclude in Configuration.ExcludeAttributes) { doc[exclude] = null; } } // Add message to documents list AppendDocument(doc); } // Limits the length of a string to Configuration.MaxLength private string LimitLength(string value) { if (value != null && value.Length > Configuration.MaxLength) value = value.Substring(0, Configuration.MaxLength - ellipsis.Length) + ellipsis; return value; } // Creates a message string from data objects private string ComposeMessage(object[] data) { string message; if (data == null || data.Length == 0) message = string.Empty; if (data.Length == 1) message = data[0].ToString(); else { StringBuilder builder = new StringBuilder(); for (int i = 0; i < data.Length; i++) { if (i != 0) { builder.Append(", "); } if (data[i] != null) { builder.Append(data[i].ToString()); } } message = builder.ToString(); } return message; } // Retrieves a non-colliding timestamp private DateTime GetCurrentTimestamp() { DateTime now = DateTime.Now; // DateTime is converted to an ISO 8601 string with millisecond precision, // so we may need to avoid range key collisions by incrementing the date by a millisecond lock (generalLock) { var diff = (now - lastTimestamp); if (diff.TotalMilliseconds < 1) now = lastTimestamp + oneMillisecond; lastTimestamp = now; } return now; } // Ensures that the writer object is created and ready for writing. // Otherwise, disables the listener. private bool EnsureWriter() { if (writer == null) { try { writer = new StreamWriter(CurrentLogFile, true, Encoding.UTF8, bufferSize); } catch (Exception e) { DisableListener("Error writing log file: " + e.ToString()); } } return IsEnabled; } // Appends a Document to the log file. private void AppendDocument(Document doc) { if (!IsEnabled) return; string json = null; try { // JsonMapper doesn't properly serialize Documents, so we store the attribute map Dictionary attributeMap = doc.ToAttributeMap(); json = JsonMapper.ToJson(attributeMap); } catch { json = null; } if (string.IsNullOrEmpty(json)) return; // Perform write lock (generalLock) { if (EnsureWriter()) { writer.WriteLine(json); } } } // Timer action for handling timed writes private void TimedWriter(object sender, System.Timers.ElapsedEventArgs e) { if (!IsEnabled) { writeTimer.Enabled = false; return; } // Update interval, as it may have been changed after the start writeTimer.Interval = Configuration.WritePeriod.TotalMilliseconds; if (IsLogFileEmpty(CurrentLogFile)) return; Flush(); } // Reads documents from the old log files. private IEnumerable GetDocuments(string path) { if (!IsEnabled || IsLogFileEmpty(path)) yield break; using (Stream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) { string line; while ((line = reader.ReadLine()) != null) { Dictionary map; try { map = JsonMapper.ToObject>(line); } catch { map = null; } if (map != null && map.Count > 0) { Document doc = Document.FromAttributeMap(map); yield return doc; } } } } // Returns true if the log file doesn't exist or has length = 0. private bool IsLogFileEmpty(string path) { var fileInfo = new FileInfo(path); return (!fileInfo.Exists || fileInfo.Length == 0); } // If file is no longer active, attempts to delete it. If that fails, empties the file out. private void DeleteLogFile(string path) { try { File.Delete(path); } catch { } if (!IsLogFileEmpty(path)) { try { File.Open(path, FileMode.Truncate, FileAccess.Write, FileShare.Read).Close(); } catch { } } } // Writes an event log message private void WriteLogMessage(string message, EventLogEntryType logEntryType) { // try the event log if (canWriteToEventLog) { WriteToEventLog(message, logEntryType); } // if event log is not available, write to a file if (!canWriteToEventLog) { WriteToEventFile(message, logEntryType); } } // Writes event to EventLog private void WriteToEventLog(string message, EventLogEntryType logEntryType) { try { EventLog.WriteEntry(eventLogSource, message, logEntryType); } catch { canWriteToEventLog = false; } } // Writes event to file, if one can be written private void WriteToEventFile(string message, EventLogEntryType logEntryType) { string logsFilePath = GetEventLogsFileLocation(); if (!string.IsNullOrEmpty(logsFilePath)) { using (var stream = File.Open(logsFilePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite)) using (var writer = new StreamWriter(stream)) { writer.WriteLine(eventLogsFormat, eventLogSource, logEntryType.ToString(), DateTime.Now.ToString(), message); } } } // Disables the DynamoDBTraceListener and writes an error event log message private void DisableListener(string message) { IsEnabled = false; WriteLogMessage("DynamoDBTraceListener disabled: " + message, EventLogEntryType.Error); } // Initializes DynamoDBTraceListener private void Init() { ConfigureEventLog(); ConfigureWriteTimer(); IsEnabled = true; } // Sets up the write timer private void ConfigureWriteTimer() { // Property Attributes isn't set yet, so set first timer to go off at minimum period writeTimer = new Timer(minWritePeriodMs); writeTimer.AutoReset = true; writeTimer.Elapsed += TimedWriter; writeTimer.Enabled = true; } // Creates source if one does not exist private void ConfigureEventLog() { try { if (!EventLog.SourceExists(eventLogSource)) EventLog.CreateEventSource(eventLogSource, "Application"); canWriteToEventLog = true; } catch { canWriteToEventLog = false; } } #endregion #region TraceListener Overrides protected override string[] GetSupportedAttributes() { return new string[] { CONFIG_ACCESSKEY, CONFIG_SECRETKEY, CONFIG_PROFILENAME, CONFIG_PROFILESLOCATION, CONFIG_REGION, CONFIG_SERVICE_URL, CONFIG_TABLE, CONFIG_CREATE_TABLE_IF_NOT_EXIST, CONFIG_READ_UNITS, CONFIG_WRITE_UNITS, CONFIG_HASHKEY, CONFIG_RANGEKEY, CONFIG_MAXLENGTH, CONFIG_EXCLUDEATTRIBUTES, CONFIG_HASHKEYFORMAT, CONFIG_RANGEKEYFORMAT, CONFIG_WRITEPERIODMS, CONFIG_LOGFILESDIR }; } public override bool IsThreadSafe { get { return false; } } public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data) { Log(eventCache, source, eventType, id, data); } public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data) { Log(eventCache, source, eventType, id, data); } public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id) { Log(eventCache, source, eventType, id); } public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args) { Log(eventCache, source, eventType, id, string.Format(format, args)); } public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message) { Log(eventCache, source, eventType, id, message); } public override void TraceTransfer(TraceEventCache eventCache, string source, int id, string message, Guid relatedActivityId) { Log(eventCache, source, TraceEventType.Transfer, id, relatedActivityId); } public override void Write(string message) { TraceData(TraceEventCache, this.Name, TraceEventType.Information, 0, message); } public override void WriteLine(string message) { TraceData(TraceEventCache, this.Name, TraceEventType.Information, 0, message); } protected override void WriteIndent() { } public override void Flush() { if (!IsEnabled) return; // Flush to disk if (writer != null) { writer.Flush(); } // Only flush log files to DynamoDB when the table is active if (!IsTableActive) return; // Get existing log files var logFiles = Directory .GetFiles(LogFileDirectory, logFileSearchPattern) .Where(p => !IsLogFileEmpty(p)); // Use new log file for subsequent logs lock (generalLock) { _currentLogFile = GetNewLogFilePath(); DisposeWriter(); } // Push log files to DynamoDB, then empty/delete them. // Each log file is sent to DynamoDB in its own batch. foreach (var oldLog in logFiles) { try { FlushLog(oldLog); } catch (Exception e) { DisableListener("Unable to write logs to DynamoDB: " + e); return; } } } public override void Close() { Flush(); base.Close(); } #endregion #region Dispose Pattern Implementation bool disposed = false; /// /// Implements the Dispose pattern /// /// Whether this object is being disposed via a call to Dispose /// or garbage collected. protected override void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { Flush(); DisposeWriter(); } IsEnabled = false; disposed = true; } } /// /// The destructor for the client class. /// ~DynamoDBTraceListener() { this.Dispose(false); } #endregion #region Attribute methods private char[] valueSeparators = { ',' }; // Retrieves a configuration from Attributes or the default configuration private Configs GetConfigFromAttributes() { // parse attributes to exclude var excludeAttributes = GetAttribute(CONFIG_EXCLUDEATTRIBUTES) .Split(valueSeparators, StringSplitOptions.RemoveEmptyEntries) // split list by valueSeparators .Select(s => (s ?? string.Empty).Trim()) // convert null to "" and trim .Where(s => s.Length > 0) // keep only non-empty strings .ToArray(); // parse write period int writePeriodMs = GetAttributeAsInt(CONFIG_WRITEPERIODMS, (int)defaultConfigs.WritePeriod.TotalMilliseconds); writePeriodMs = Math.Max(writePeriodMs, minWritePeriodMs); // construct configs from attributes var configs = new Configs { AWSAccessKey = GetAttribute(CONFIG_ACCESSKEY), AWSSecretKey = GetAttribute(CONFIG_SECRETKEY), AWSProfileName = GetAttribute(CONFIG_PROFILENAME), AWSProfilesLocation = GetAttribute(CONFIG_PROFILESLOCATION), Region = RegionEndpoint.GetBySystemName(GetAttribute(CONFIG_REGION, defaultConfigs.Region.SystemName)), ServiceURL = GetAttribute(CONFIG_SERVICE_URL), TableName = GetAttribute(CONFIG_TABLE, defaultConfigs.TableName), ReadUnits = GetAttributeAsInt(CONFIG_READ_UNITS, defaultConfigs.ReadUnits), WriteUnits = GetAttributeAsInt(CONFIG_WRITE_UNITS, defaultConfigs.WriteUnits), CreateTableIfNotExist = GetAttributeAsBool(CONFIG_CREATE_TABLE_IF_NOT_EXIST, defaultConfigs.CreateTableIfNotExist), MaxLength = GetAttributeAsInt(CONFIG_MAXLENGTH, defaultConfigs.MaxLength), HashKeyFormat = GetAttribute(CONFIG_HASHKEYFORMAT, defaultConfigs.HashKeyFormat), RangeKeyFormat = GetAttribute(CONFIG_RANGEKEYFORMAT, defaultConfigs.RangeKeyFormat), HashKey = GetAttribute(CONFIG_HASHKEY, defaultConfigs.HashKey), RangeKey = GetAttribute(CONFIG_RANGEKEY, defaultConfigs.RangeKey), WritePeriod = TimeSpan.FromMilliseconds(writePeriodMs), ExcludeAttributes = excludeAttributes, LogFilesDirectory = GetAttribute(CONFIG_LOGFILESDIR, defaultConfigs.LogFilesDirectory) }; return configs; } private bool GetAttributeAsBool(string name, bool defaultValue = false) { string value = GetAttribute(name); bool b; if (string.IsNullOrEmpty(value) || !bool.TryParse(value, out b)) b = defaultValue; return b; } private int GetAttributeAsInt(string name, int defaultValue = -1) { string value = GetAttribute(name); int i; if (string.IsNullOrEmpty(value) || !int.TryParse(value, out i)) i = defaultValue; return i; } private string GetAttribute(string name, string defaultValue = "") { if (Attributes.ContainsKey(name)) return Attributes[name]; else return defaultValue; } #endregion #region Constructor /// /// Constructs an instance of the DynamoDBTraceListener. /// public DynamoDBTraceListener() : base() { Init(); } /// /// Constructs a named instance of the DynamoDBTraceListener. /// public DynamoDBTraceListener(string name) : base(name) { Init(); } #endregion #region Public methods /// /// Flushes an existing log to DynamoDB, then deletes/empties the log file. /// This method can be invoked manually to flush left-over log files. /// /// public void FlushLog(string log) { // Get Documents stored in log file var documents = GetDocuments(log); // Create, populate and execute BatchWrite var batchWrite = Table.CreateBatchWrite(); foreach (var doc in documents) { batchWrite.AddDocumentToPut(doc); } // Attempt to write (may throw exception) batchWrite.Execute(); // If BatchWrite succeeded, empty old log file DeleteLogFile(log); } #endregion #region Public classes /// /// DynamoDBTraceListener configurations. /// public class Configs { #region Login/table properties /// /// Access key to use. /// Config key: AWSAccessKey /// public string AWSAccessKey { get; set; } /// /// Secret key to use. /// Config key: AWSSecretKey /// public string AWSSecretKey { get; set; } /// /// Profile to use. /// Config key: AWSProfileName /// public string AWSProfileName { get; set; } /// /// Location of credentials file. /// Config key: AWSProfilesLocation /// public string AWSProfilesLocation { get; set; } /// /// Credentials to use. /// This property can only be set programmatically. /// public AWSCredentials AWSCredentials { get; set; } /// /// Region for the table. Default is US West 2 (Oregon). /// Config key: Region /// public RegionEndpoint Region { get; set; } /// /// The URL of the DynamoDB endpoint. This can be used instead of region. This property is commonly used for connecting to DynamoDB Local (e.g. http://localhost:8000/) /// Config key: ServiceURL /// public string ServiceURL { get; set; } /// /// Table used to store logs. Default is "Logs". /// Config key: Table /// public string TableName { get; set; } #endregion #region Table-creation properties /// /// Controls whether the table will be auto created if it doesn't exist. The default is true. /// Config key: CreateIfNotExist /// public bool CreateTableIfNotExist { get; set; } /// /// The read capacity units if the table is created. The default is 1. /// Config key: ReadCapacityUnits /// public int ReadUnits { get; set; } /// /// The write capacity units if the table is created. The default is 10. /// Config key: WriteCapacityUnits /// public int WriteUnits { get; set; } /// /// The hash-key name if the table is created. The default is "Origin". /// Config key: HashKey /// public string HashKey { get; set; } /// /// The range-key name if the table is created. The default is "Timestamp". /// Config key: RangeKey /// public string RangeKey { get; set; } #endregion #region Message/logging properties /// /// The maximum length of any one attribute. The default is 10,000 characters. /// Config key: MaxLength /// public int MaxLength { get; set; } /// /// Format of the hash key. The default is "{Host}". /// The format can include environment variables, attributes from the logged message, or can /// be a constant value. /// /// Environment variables are specified like so: %ComputerName% /// Attributes are specified like so: {Host} /// For example: {Host}-{EventType}-{ProcessId} or %ComputerName%-{EventType}-{ProcessId} /// /// Config key: HashKeyFormat /// public string HashKeyFormat { get; set; } /// /// Format of the range key. The default is "{Time}". /// The format can include environment variables, attributes from the logged message, or can /// be a constant value. /// /// Environment variables are specified like so: %Time% /// Attributes are specified like so: {Time} /// /// Config key: RangeKeyFormat /// public string RangeKeyFormat { get; set; } /// /// Array of attributes to exclude from the logged item. Default is null. /// Config key: ExcludeAttributes (comma-separated list of attribute names) /// public string[] ExcludeAttributes { get; set; } /// /// Largest time between writes to DynamoDB. /// If this period has passed since the last log write, all accumulated messages /// are written to DynamoDB immediately. /// /// The listener only pushes messages to DynamoDB when the target table is active /// AND one of the following happens: /// 1. The time equal to WritePeriod since last write has elapsed /// 2. Flush is called /// 3. Close is called /// /// Default value is 1 minute. Smallest allowed value is 0 seconds. /// /// Config key: WritePeriodMs (number of milliseconds) /// public TimeSpan WritePeriod { get; set; } /// /// The directory that temporary log files should be written to. /// If the value is not specified, listener attempts to use the application's current /// directory, then the Windows temporary directory. If none of these are accessible, /// the listener will be disabled and an error message will be written to the error log. /// /// Default value is null. /// /// Config key: LogFilesDir /// public string LogFilesDirectory { get; set; } #endregion #region Constructor /// /// Constructs a default Configs instance. /// public Configs() { HashKey = "Origin"; RangeKey = "Timestamp"; TableName = "Logs"; ReadUnits = 1; WriteUnits = 10; CreateTableIfNotExist = true; MaxLength = 10 * 1000; Region = RegionEndpoint.USWest2; HashKeyFormat = "{Host}"; RangeKeyFormat = "{Time}"; WritePeriod = TimeSpan.FromMinutes(1); LogFilesDirectory = null; } #endregion } #endregion } }