Skip to main content
Glama
AILogger.cs13.9 kB
using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace AI.Logging { /// <summary> /// Log levels supported by the AI Logger /// </summary> public enum LogLevel { Debug = 0, Info = 1, Warning = 2, Error = 3, Critical = 4 } /// <summary> /// Configuration for the AI Logger /// </summary> public class AILoggerConfig { /// <summary> /// URL of the MCP server /// </summary> public string ServerUrl { get; set; } = "http://localhost:8080"; /// <summary> /// Default log level /// </summary> public LogLevel DefaultLogLevel { get; set; } = LogLevel.Info; /// <summary> /// Size of the log buffer before flushing /// </summary> public int BufferSize { get; set; } = 100; /// <summary> /// Interval to flush the log buffer /// </summary> public TimeSpan FlushInterval { get; set; } = TimeSpan.FromSeconds(5); /// <summary> /// Whether to include call site information /// </summary> public bool IncludeCallSite { get; set; } = true; /// <summary> /// Whether to include timestamp /// </summary> public bool IncludeTimestamp { get; set; } = true; } /// <summary> /// Log entry /// </summary> public class LogEntry { /// <summary> /// Log level /// </summary> public LogLevel Level { get; set; } /// <summary> /// Logger name /// </summary> public string LoggerName { get; set; } /// <summary> /// Log message /// </summary> public string Message { get; set; } /// <summary> /// Log data /// </summary> public object Data { get; set; } /// <summary> /// Log timestamp /// </summary> public DateTime Timestamp { get; set; } /// <summary> /// Log tags /// </summary> public List<string> Tags { get; set; } = new List<string>(); /// <summary> /// Log context /// </summary> public Dictionary<string, object> Context { get; set; } = new Dictionary<string, object>(); /// <summary> /// Call site information /// </summary> public string CallSite { get; set; } } /// <summary> /// Interface for log appenders /// </summary> public interface ILogAppender { /// <summary> /// Append a log entry /// </summary> /// <param name="entry">Log entry</param> void Append(LogEntry entry); } /// <summary> /// MCP server appender /// </summary> public class MCPServerAppender : ILogAppender { private readonly string serverUrl; private readonly HttpClient httpClient; private readonly List<LogEntry> buffer = new List<LogEntry>(); private readonly int bufferSize; private readonly Timer flushTimer; private readonly object lockObject = new object(); /// <summary> /// Create a new MCP server appender /// </summary> /// <param name="serverUrl">URL of the MCP server</param> /// <param name="bufferSize">Size of the log buffer before flushing</param> /// <param name="flushInterval">Interval to flush the log buffer</param> public MCPServerAppender(string serverUrl, int bufferSize = 100, TimeSpan? flushInterval = null) { this.serverUrl = serverUrl; this.bufferSize = bufferSize; this.httpClient = new HttpClient(); this.flushTimer = new Timer(FlushTimerCallback, null, flushInterval ?? TimeSpan.FromSeconds(5), flushInterval ?? TimeSpan.FromSeconds(5)); } /// <summary> /// Append a log entry /// </summary> /// <param name="entry">Log entry</param> public void Append(LogEntry entry) { lock (lockObject) { buffer.Add(entry); if (buffer.Count >= bufferSize) { Flush(); } } } /// <summary> /// Flush the log buffer /// </summary> public void Flush() { List<LogEntry> entriesToFlush; lock (lockObject) { if (buffer.Count == 0) { return; } entriesToFlush = new List<LogEntry>(buffer); buffer.Clear(); } try { // Group entries by logger name var entriesByLogger = new Dictionary<string, List<LogEntry>>(); foreach (var entry in entriesToFlush) { if (!entriesByLogger.ContainsKey(entry.LoggerName)) { entriesByLogger[entry.LoggerName] = new List<LogEntry>(); } entriesByLogger[entry.LoggerName].Add(entry); } // Send entries to MCP server foreach (var kvp in entriesByLogger) { var logName = kvp.Key; var entries = kvp.Value; var json = JsonSerializer.Serialize(entries); var content = new StringContent(json, Encoding.UTF8, "application/json"); var task = httpClient.PostAsync($"{serverUrl}/logs/{logName}/append", content); task.Wait(); } } catch (Exception ex) { Console.Error.WriteLine($"Error flushing logs: {ex.Message}"); } } private void FlushTimerCallback(object state) { Flush(); } } /// <summary> /// Console appender /// </summary> public class ConsoleAppender : ILogAppender { /// <summary> /// Append a log entry /// </summary> /// <param name="entry">Log entry</param> public void Append(LogEntry entry) { var color = GetColorForLevel(entry.Level); Console.ForegroundColor = color; Console.WriteLine($"[{entry.Timestamp:yyyy-MM-dd HH:mm:ss}] [{entry.Level}] [{entry.LoggerName}] {entry.Message}"); if (entry.Data != null) { Console.WriteLine($"Data: {JsonSerializer.Serialize(entry.Data)}"); } Console.ResetColor(); } private ConsoleColor GetColorForLevel(LogLevel level) { switch (level) { case LogLevel.Debug: return ConsoleColor.Gray; case LogLevel.Info: return ConsoleColor.White; case LogLevel.Warning: return ConsoleColor.Yellow; case LogLevel.Error: return ConsoleColor.Red; case LogLevel.Critical: return ConsoleColor.DarkRed; default: return ConsoleColor.White; } } } /// <summary> /// AI Logger /// </summary> public class AILogger { private readonly string loggerName; private readonly AILoggerConfig config; private readonly List<ILogAppender> appenders = new List<ILogAppender>(); private readonly List<string> tags = new List<string>(); private readonly Dictionary<string, object> context = new Dictionary<string, object>(); /// <summary> /// Create a new AI Logger /// </summary> /// <param name="loggerName">Logger name</param> /// <param name="config">Logger configuration</param> public AILogger(string loggerName, AILoggerConfig config = null) { this.loggerName = loggerName; this.config = config ?? new AILoggerConfig(); // Add default appender if none provided if (appenders.Count == 0) { AddAppender(new MCPServerAppender( this.config.ServerUrl, this.config.BufferSize, this.config.FlushInterval )); } } /// <summary> /// Add an appender to the logger /// </summary> /// <param name="appender">Appender to add</param> public void AddAppender(ILogAppender appender) { appenders.Add(appender); } /// <summary> /// Create a new logger with the specified tags /// </summary> /// <param name="tags">Tags to add</param> /// <returns>New logger with the specified tags</returns> public AILogger WithTags(params string[] tags) { var logger = new AILogger(loggerName, config); foreach (var appender in appenders) { logger.AddAppender(appender); } logger.tags.AddRange(this.tags); logger.tags.AddRange(tags); foreach (var kvp in context) { logger.context[kvp.Key] = kvp.Value; } return logger; } /// <summary> /// Create a new logger with the specified context /// </summary> /// <param name="key">Context key</param> /// <param name="value">Context value</param> /// <returns>New logger with the specified context</returns> public AILogger WithContext(string key, object value) { var logger = new AILogger(loggerName, config); foreach (var appender in appenders) { logger.AddAppender(appender); } logger.tags.AddRange(this.tags); foreach (var kvp in context) { logger.context[kvp.Key] = kvp.Value; } logger.context[key] = value; return logger; } /// <summary> /// Log a message at the specified level /// </summary> /// <param name="level">Log level</param> /// <param name="message">Log message</param> /// <param name="data">Log data</param> public void Log(LogLevel level, string message, object data = null) { if (level < config.DefaultLogLevel) { return; } var entry = new LogEntry { Level = level, LoggerName = loggerName, Message = message, Data = data, Timestamp = DateTime.UtcNow, Tags = new List<string>(tags), Context = new Dictionary<string, object>(context) }; if (config.IncludeCallSite) { entry.CallSite = GetCallSite(); } foreach (var appender in appenders) { appender.Append(entry); } } /// <summary> /// Log a debug message /// </summary> /// <param name="message">Log message</param> /// <param name="data">Log data</param> public void Debug(string message, object data = null) { Log(LogLevel.Debug, message, data); } /// <summary> /// Log an info message /// </summary> /// <param name="message">Log message</param> /// <param name="data">Log data</param> public void Info(string message, object data = null) { Log(LogLevel.Info, message, data); } /// <summary> /// Log a warning message /// </summary> /// <param name="message">Log message</param> /// <param name="data">Log data</param> public void Warning(string message, object data = null) { Log(LogLevel.Warning, message, data); } /// <summary> /// Log an error message /// </summary> /// <param name="message">Log message</param> /// <param name="data">Log data</param> public void Error(string message, object data = null) { Log(LogLevel.Error, message, data); } /// <summary> /// Log a critical message /// </summary> /// <param name="message">Log message</param> /// <param name="data">Log data</param> public void Critical(string message, object data = null) { Log(LogLevel.Critical, message, data); } private string GetCallSite() { try { var stackTrace = new System.Diagnostics.StackTrace(true); var frames = stackTrace.GetFrames(); // Skip frames for the logger itself for (int i = 0; i < frames.Length; i++) { var method = frames[i].GetMethod(); var declaringType = method.DeclaringType; if (declaringType != typeof(AILogger)) { var fileName = frames[i].GetFileName(); var lineNumber = frames[i].GetFileLineNumber(); return $"{declaringType.FullName}.{method.Name} ({fileName}:{lineNumber})"; } } } catch { // Ignore errors in getting call site } return null; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TSavo/Unity-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server