diff --git a/src/ApiService/ApiService/EnvironmentVariables.cs b/src/ApiService/ApiService/EnvironmentVariables.cs index 35251b0f3..dd9b943a7 100644 --- a/src/ApiService/ApiService/EnvironmentVariables.cs +++ b/src/ApiService/ApiService/EnvironmentVariables.cs @@ -1,8 +1,21 @@ using System; namespace Microsoft.OneFuzz.Service; +public enum LogDestination +{ + Console, + AppInsights, +} + public static class EnvironmentVariables { + static EnvironmentVariables() { + LogDestinations = new LogDestination[] { LogDestination.AppInsights }; + } + + //TODO: Add environment variable to control where to write logs to + public static LogDestination[] LogDestinations { get; set; } + public static class AppInsights { public static string? AppId { get => Environment.GetEnvironmentVariable("APPINSIGHTS_APPID"); } public static string? InstrumentationKey { get => Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY"); } diff --git a/src/ApiService/ApiService/Log.cs b/src/ApiService/ApiService/Log.cs new file mode 100644 index 000000000..7ea69c0d1 --- /dev/null +++ b/src/ApiService/ApiService/Log.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights.DataContracts; + +using System.Diagnostics; + +namespace Microsoft.OneFuzz.Service; + +public interface ILog { + void Log(Guid correlationId, String message, SeverityLevel level, IDictionary tags, string? caller); + void LogEvent(Guid correlationId, String evt, IDictionary tags, IDictionary? metrics, string? caller); + void LogException(Guid correlationId, Exception ex, IDictionary tags, IDictionary? metrics, string? caller); + void Flush(); +} + +class AppInsights : ILog { + private TelemetryClient telemetryClient = + new TelemetryClient( + new TelemetryConfiguration(EnvironmentVariables.AppInsights.InstrumentationKey)); + + public void Log(Guid correlationId, String message, SeverityLevel level, IDictionary tags, string? caller) { + tags.Add("Correlation ID", correlationId.ToString()); + if (caller is not null) tags.Add("CalledBy", caller); + telemetryClient.TrackTrace(message, level, tags); + } + public void LogEvent(Guid correlationId, String evt, IDictionary tags, IDictionary? metrics, string? caller) { + tags.Add("Correlation ID", correlationId.ToString()); + if (caller is not null) tags.Add("CalledBy", caller); + telemetryClient.TrackEvent(evt, properties: tags, metrics: metrics); + } + public void LogException(Guid correlationId, Exception ex, IDictionary tags, IDictionary? metrics, string? caller) { + tags.Add("Correlation ID", correlationId.ToString()); + + if (caller is not null) tags.Add("CalledBy", caller); + telemetryClient.TrackException(ex, tags, metrics); + } + + public void Flush() { + telemetryClient.Flush(); + } +} + +//TODO: Should we write errors and Exception to std err ? +class Console : ILog { + + private string DictToString(IDictionary? d) { + if (d is null) + { + return string.Empty; + } + else + { + return string.Join("", d); + } + } + + private void LogTags(Guid correlationId, string? caller, IDictionary tags) { + var ts = DictToString(tags); + if (!string.IsNullOrEmpty(ts)) + { + System.Console.WriteLine("[{0}:{1}] Tags:{2}", correlationId, caller, ts); + } + } + + private void LogMetrics(Guid correlationId, string? caller, IDictionary? metrics) { + var ms = DictToString(metrics); + if (!string.IsNullOrEmpty(ms)) { + System.Console.Out.WriteLine("[{0}:{1}] Metrics:{2}", correlationId, caller, DictToString(metrics)); + } + } + + public void Log(Guid correlationId, String message, SeverityLevel level, IDictionary tags, string? caller) { + System.Console.Out.WriteLine("[{0}:{1}][{2}] {3}", correlationId, caller, level, message); + LogTags(correlationId, caller, tags); + } + + public void LogEvent(Guid correlationId, String evt, IDictionary tags, IDictionary? metrics, string? caller) { + System.Console.Out.WriteLine("[{0}:{1}][Event] {2}", correlationId, caller, evt); + LogTags(correlationId, caller, tags); + LogMetrics(correlationId, caller, metrics); + } + public void LogException(Guid correlationId, Exception ex, IDictionary tags, IDictionary? metrics, string? caller) { + System.Console.Out.WriteLine("[{0}:{1}][Exception] {2}", correlationId, caller, ex); + LogTags(correlationId, caller, tags); + LogMetrics(correlationId, caller, metrics); + } + public void Flush() { + System.Console.Out.Flush(); + } +} + +public class LogTracer { + + private List loggers; + + private IDictionary tags = new Dictionary(); + private Guid correlationId; + + public LogTracer(Guid correlationId, List loggers) { + this.correlationId = correlationId; + this.loggers = loggers; + } + + public IDictionary Tags => tags; + + public void Info(string message) { + var caller = new StackTrace()?.GetFrame(1)?.GetMethod()?.Name; + foreach (var logger in loggers) { + logger.Log(correlationId, message, SeverityLevel.Information, Tags, caller); + } + } + + public void Warning(string message) { + var caller = new StackTrace()?.GetFrame(1)?.GetMethod()?.Name; + foreach (var logger in loggers) { + logger.Log(correlationId, message, SeverityLevel.Warning, Tags, caller); + } + } + + public void Error(string message) + { + var caller = new StackTrace()?.GetFrame(1)?.GetMethod()?.Name; + foreach (var logger in loggers) + { + logger.Log(correlationId, message, SeverityLevel.Error, Tags, caller); + } + } + + public void Critical(string message) + { + var caller = new StackTrace()?.GetFrame(1)?.GetMethod()?.Name; + foreach (var logger in loggers) + { + logger.Log(correlationId, message, SeverityLevel.Critical, Tags, caller); + } + } + + public void Event(string evt, IDictionary? metrics) { + var caller = new StackTrace()?.GetFrame(1)?.GetMethod()?.Name; + foreach (var logger in loggers) { + logger.LogEvent(correlationId, evt, Tags, metrics, caller); + } + } + + public void Exception(Exception ex, IDictionary? metrics) { + var caller = new StackTrace()?.GetFrame(1)?.GetMethod()?.Name; + foreach (var logger in loggers) { + logger.LogException(correlationId, ex, Tags, metrics, caller); + } + } + + public void ForceFlush() { + foreach (var logger in loggers) { + logger.Flush(); + } + } +} + +public class LogTracerFactory { + + private List loggers; + + public LogTracerFactory(List loggers) { + this.loggers = loggers; + } + + public LogTracer MakeLogTracer(Guid correlationId) { + return new LogTracer(correlationId, this.loggers); + } + +} diff --git a/src/ApiService/ApiService/Program.cs b/src/ApiService/ApiService/Program.cs index cf0193920..62dc2225d 100644 --- a/src/ApiService/ApiService/Program.cs +++ b/src/ApiService/ApiService/Program.cs @@ -1,17 +1,41 @@ +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Azure.Functions.Worker.Configuration; +using Azure.ResourceManager.Storage.Models; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.OneFuzz.Service; public class Program { + public static List GetLoggers() { + List loggers = new List(); + foreach (var dest in EnvironmentVariables.LogDestinations) + { + loggers.Add( + dest switch + { + LogDestination.AppInsights => new AppInsights(), + LogDestination.Console => new Console(), + _ => throw new Exception(string.Format("Unhandled Log Destination type: {0}", dest)), + } + ); + } + return loggers; + } + + public static void Main() { var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .Build(); + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices((context, services) => + services.AddSingleton(_ => new LogTracerFactory(GetLoggers())) + ) + .Build(); host.Run(); }