Files
runner/src/Runner.Common/HostTraceListener.cs
2022-10-18 10:54:08 -04:00

202 lines
6.9 KiB
C#

using GitHub.Runner.Sdk;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
namespace GitHub.Runner.Common
{
public sealed class HostTraceListener : TextWriterTraceListener
{
private const string _logFileNamingPattern = "{0}_{1:yyyyMMdd-HHmmss}-utc.log";
private string _logFileDirectory;
private string _logFilePrefix;
private bool _enablePageLog = false;
private bool _enableLogRetention = false;
private int _currentPageSize;
private int _pageSizeLimit;
private int _retentionDays;
public HostTraceListener(string logFileDirectory, string logFilePrefix, int pageSizeLimit, int retentionDays)
: base()
{
ArgUtil.NotNullOrEmpty(logFileDirectory, nameof(logFileDirectory));
ArgUtil.NotNullOrEmpty(logFilePrefix, nameof(logFilePrefix));
_logFileDirectory = logFileDirectory;
_logFilePrefix = logFilePrefix;
Directory.CreateDirectory(_logFileDirectory);
if (pageSizeLimit > 0)
{
_enablePageLog = true;
_pageSizeLimit = pageSizeLimit * 1024 * 1024;
_currentPageSize = 0;
}
if (retentionDays > 0)
{
_enableLogRetention = true;
_retentionDays = retentionDays;
}
Writer = CreatePageLogWriter();
}
public HostTraceListener(string logFile)
: base()
{
ArgUtil.NotNullOrEmpty(logFile, nameof(logFile));
Directory.CreateDirectory(Path.GetDirectoryName(logFile));
Stream logStream = new FileStream(logFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 4096);
Writer = new StreamWriter(logStream);
}
// Copied and modified slightly from .Net Core source code. Modification was required to make it compile.
// There must be some TraceFilter extension class that is missing in this source code.
public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
{
if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, message, null, null, null))
{
return;
}
WriteHeader(source, eventType, id);
WriteLine(message);
WriteFooter(eventCache);
}
public override void WriteLine(string message)
{
base.WriteLine(message);
if (_enablePageLog)
{
int messageSize = UTF8Encoding.UTF8.GetByteCount(message);
_currentPageSize += messageSize;
if (_currentPageSize > _pageSizeLimit)
{
Flush();
if (Writer != null)
{
Writer.Dispose();
Writer = null;
}
Writer = CreatePageLogWriter();
_currentPageSize = 0;
}
}
Flush();
}
public override void Write(string message)
{
base.Write(message);
if (_enablePageLog)
{
int messageSize = UTF8Encoding.UTF8.GetByteCount(message);
_currentPageSize += messageSize;
}
Flush();
}
internal bool IsEnabled(TraceOptions opts)
{
return (opts & TraceOutputOptions) != 0;
}
// Altered from the original .Net Core implementation.
private void WriteHeader(string source, TraceEventType eventType, int id)
{
string type = null;
switch (eventType)
{
case TraceEventType.Critical:
type = "CRIT";
break;
case TraceEventType.Error:
type = "ERR ";
break;
case TraceEventType.Warning:
type = "WARN";
break;
case TraceEventType.Information:
type = "INFO";
break;
case TraceEventType.Verbose:
type = "VERB";
break;
default:
type = eventType.ToString();
break;
}
Write(StringUtil.Format("[{0:u} {1} {2}] ", DateTime.UtcNow, type, source));
}
// Copied and modified slightly from .Net Core source code to make it compile. The original code
// accesses a private indentLevel field. In this code it has been modified to use the getter/setter.
private void WriteFooter(TraceEventCache eventCache)
{
if (eventCache == null)
return;
IndentLevel++;
if (IsEnabled(TraceOptions.ProcessId))
WriteLine("ProcessId=" + eventCache.ProcessId);
if (IsEnabled(TraceOptions.ThreadId))
WriteLine("ThreadId=" + eventCache.ThreadId);
if (IsEnabled(TraceOptions.DateTime))
WriteLine("DateTime=" + eventCache.DateTime.ToString("o", CultureInfo.InvariantCulture));
if (IsEnabled(TraceOptions.Timestamp))
WriteLine("Timestamp=" + eventCache.Timestamp);
IndentLevel--;
}
private StreamWriter CreatePageLogWriter()
{
if (_enableLogRetention)
{
DirectoryInfo diags = new(_logFileDirectory);
var logs = diags.GetFiles($"{_logFilePrefix}*.log");
foreach (var log in logs)
{
if (log.LastWriteTimeUtc.AddDays(_retentionDays) < DateTime.UtcNow)
{
try
{
log.Delete();
}
catch (Exception)
{
// catch Exception and continue
// we shouldn't block logging and fail the runner if the runner can't delete an older log file.
}
}
}
}
string fileName = StringUtil.Format(_logFileNamingPattern, _logFilePrefix, DateTime.UtcNow);
string logFile = Path.Combine(_logFileDirectory, fileName);
Stream logStream;
if (File.Exists(logFile))
{
logStream = new FileStream(logFile, FileMode.Append, FileAccess.Write, FileShare.Read, bufferSize: 4096);
}
else
{
logStream = new FileStream(logFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 4096);
}
return new StreamWriter(logStream);
}
}
}