diff --git a/Obsidian.GuiConsole/GlobalUsings.cs b/Obsidian.GuiConsole/GlobalUsings.cs
new file mode 100644
index 000000000..a975fd2c3
--- /dev/null
+++ b/Obsidian.GuiConsole/GlobalUsings.cs
@@ -0,0 +1,4 @@
+global using System;
+global using System.Collections.Generic;
+global using System.IO;
+global using System.Threading.Tasks;
diff --git a/Obsidian.GuiConsole/Logger/TerminalGuiLogger.cs b/Obsidian.GuiConsole/Logger/TerminalGuiLogger.cs
new file mode 100644
index 000000000..a01ed916c
--- /dev/null
+++ b/Obsidian.GuiConsole/Logger/TerminalGuiLogger.cs
@@ -0,0 +1,41 @@
+using Microsoft.Extensions.Logging;
+using Obsidian.GuiConsole.Window;
+using Terminal.Gui.Views;
+
+namespace Obsidian.GuiConsole.Logger;
+
+///
+/// Logger implementation that logs messages to the Terminal GUI Console
+///
+public class TerminalGuiLogger : ILogger
+{
+ private readonly ObsidianConsole _console;
+ private readonly LogLevel _minLevel;
+
+ public TerminalGuiLogger(ObsidianConsole console, LogLevel minLevel)
+ {
+ _console = console;
+ _minLevel = minLevel;
+ }
+
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+
+ public bool IsEnabled(LogLevel logLevel) => logLevel >= _minLevel;
+ ///
+ /// Log message to the GUI Console
+ ///
+ /// level
+ /// evt
+ /// state
+ /// error
+ /// format
+ /// type of state
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter)
+ {
+ if (!IsEnabled(logLevel)) return;
+
+ var message = formatter(state, exception);
+
+ _console.AppendLog(logLevel,message);
+ }
+}
diff --git a/Obsidian.GuiConsole/Logger/TerminalGuiLoggerProvider.cs b/Obsidian.GuiConsole/Logger/TerminalGuiLoggerProvider.cs
new file mode 100644
index 000000000..b5843930d
--- /dev/null
+++ b/Obsidian.GuiConsole/Logger/TerminalGuiLoggerProvider.cs
@@ -0,0 +1,25 @@
+using Microsoft.Extensions.Logging;
+using Obsidian.GuiConsole.Window;
+
+namespace Obsidian.GuiConsole.Logger;
+
+public class TerminalGuiLoggerProvider : ILoggerProvider
+{
+ private readonly ObsidianConsole _console;
+ private readonly LogLevel _minLevel;
+
+ public TerminalGuiLoggerProvider(ObsidianConsole console, LogLevel minLevel)
+ {
+ _console = console;
+ _minLevel = minLevel;
+ }
+
+ ///
+ /// Create a logger for the specified category
+ ///
+ ///
+ ///
+ public ILogger CreateLogger(string categoryName) => new TerminalGuiLogger(_console, _minLevel);
+
+ public void Dispose() { }
+}
diff --git a/Obsidian.GuiConsole/Obsidian.GuiConsole.csproj b/Obsidian.GuiConsole/Obsidian.GuiConsole.csproj
new file mode 100644
index 000000000..803106445
--- /dev/null
+++ b/Obsidian.GuiConsole/Obsidian.GuiConsole.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Obsidian.GuiConsole/Program.Functions.cs b/Obsidian.GuiConsole/Program.Functions.cs
new file mode 100644
index 000000000..4ea560b88
--- /dev/null
+++ b/Obsidian.GuiConsole/Program.Functions.cs
@@ -0,0 +1,58 @@
+using System.Reflection;
+
+namespace Obsidian.GuiConsole;
+
+public partial class Program
+{
+ ///
+ /// release default config files if not exist
+ ///
+ private static async ValueTask GenerateConfigFiles()
+ {
+ const string path = "config";
+
+ Directory.CreateDirectory(path);
+
+ var serverJsonFile = Path.Combine(path, "server.json");
+ var whitelistJsonFile = Path.Combine(path, "whitelist.json");
+
+ if (!File.Exists(serverJsonFile))
+ {
+ await using var file = File.Create(serverJsonFile);
+
+ await using var embeddedFile = Assembly.GetExecutingAssembly().GetManifestResourceStream("Obsidian.GuiConsole.config.server.json");
+
+ await embeddedFile!.CopyToAsync(file);
+ }
+
+ if (!File.Exists(whitelistJsonFile))
+ {
+ await using var file = File.Create(whitelistJsonFile);
+
+ await using var embeddedFile = Assembly.GetExecutingAssembly().GetManifestResourceStream("Obsidian.GuiConsole.config.whitelist.json");
+
+ await embeddedFile!.CopyToAsync(file);
+ }
+ }
+
+ ///
+ /// Logo is cool,but not render well on my machine -- stevesensei
+ ///
+ public static void DrawLogo()
+ {
+ const string asciilogo =
+ "\n" +
+ " ▄▄▄▄ ▄▄ ▄▄▄▄ ▀ ▄▄▄ ▐ ▄ \n" +
+ " ▐█ ▀█ ▐█ ▀ ██ ██ ██ ██ ▐█ ▀█ █▌▐█\n" +
+ " ▄█▀▄ ▐█▀▀█▄▄▀▀▀█▄▐█ ▐█ ▐█▌▐█ ▄█▀▀█ ▐█▐▐▌\n" +
+ "▐█▌ ▐▌██▄ ▐█▐█▄ ▐█▐█▌██ ██ ▐█▌▐█ ▐▌██▐█▌\n" +
+ " ▀█▄▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀▀▀▀ ▀▀▀ ▀ ▀ ▀▀ █ \n\n";
+
+ Console.Title = $"Obsidian for {ServerConstants.DefaultProtocol} ({ServerConstants.VERSION})";
+ Console.BackgroundColor = ConsoleColor.White;
+ Console.ForegroundColor = ConsoleColor.Black;
+ Console.CursorVisible = false;
+ Console.WriteLine(asciilogo);
+ Console.ResetColor();
+ }
+}
diff --git a/Obsidian.GuiConsole/Program.cs b/Obsidian.GuiConsole/Program.cs
new file mode 100644
index 000000000..5e145358c
--- /dev/null
+++ b/Obsidian.GuiConsole/Program.cs
@@ -0,0 +1,74 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Obsidian.GuiConsole.Logger;
+using Obsidian.GuiConsole.Services;
+using Obsidian.GuiConsole.Services.Command;
+using Obsidian.GuiConsole.Window;
+using Obsidian.Hosting;
+using Terminal.Gui.App;
+using Terminal.Gui.Configuration;
+
+namespace Obsidian.GuiConsole;
+
+public partial class Program
+{
+ ///
+ /// entry point
+ ///
+ /// currently useless
+ public static async Task Main(string[] args)
+ {
+ //Cool logo,but not render well on my machine -- stevesensei
+ DrawLogo();
+
+ //Init Gui App
+ ConfigurationManager.Enable(ConfigLocations.All);
+
+ //Create console window
+
+
+ //Normal Obsidian setup
+ await GenerateConfigFiles();
+ var builder = Host.CreateApplicationBuilder();
+ builder.ConfigureObsidian();
+ if (!Directory.Exists("logs"))
+ {
+ Directory.CreateDirectory("logs");
+ }
+
+ //Add Obsidian with GUI logger
+ var console = new ObsidianConsole();
+
+ builder.AddObsidianWithGui(x =>
+ {
+ x.AddProvider(new TerminalGuiLoggerProvider(console, LogLevel.Information));
+ return x;
+ });
+
+ //Give the server some time to shut down after CTRL-C or SIGTERM.
+ builder.Services.Configure(opts =>
+ {
+ opts.ShutdownTimeout = TimeSpan.FromSeconds(10);
+ });
+ builder.Services.AddSingleton();
+ var hostApp = builder.Build();
+
+ IApplication? app = null;
+ try
+ {
+ //Run the application
+ await hostApp.StartAsync();
+ var commandMiddleware = hostApp.Services.GetRequiredService();
+ console.AddCommandMiddleware(commandMiddleware);
+ app = Application.Create().Init();
+ app.Run(console);
+ }
+ finally
+ {
+ //Cleanup when application stops
+ await hostApp.StopAsync();
+ app?.Dispose();
+ }
+ }
+}
diff --git a/Obsidian.GuiConsole/Services/Command/CommandMiddleware.cs b/Obsidian.GuiConsole/Services/Command/CommandMiddleware.cs
new file mode 100644
index 000000000..51b722920
--- /dev/null
+++ b/Obsidian.GuiConsole/Services/Command/CommandMiddleware.cs
@@ -0,0 +1,23 @@
+using Microsoft.Extensions.Logging;
+using Obsidian.API;
+using Obsidian.API.Commands;
+
+namespace Obsidian.GuiConsole.Services.Command;
+
+///
+/// Command middleware to process commands from GUI console
+///
+/// Server Instance
+/// log
+public class CommandMiddleware(IServer server,ILogger logger)
+{
+ public async Task ExecuteCommandFromConsoleAsync(string commandText)
+ {
+ await server.CommandHandler.ProcessCommand(new CommandContext(
+ "/"+commandText,
+ new ConsoleCommandSender(logger),null,
+ server
+ ));
+ }
+
+}
diff --git a/Obsidian.GuiConsole/Services/Command/ConsoleCommandSender.cs b/Obsidian.GuiConsole/Services/Command/ConsoleCommandSender.cs
new file mode 100644
index 000000000..83c86e43b
--- /dev/null
+++ b/Obsidian.GuiConsole/Services/Command/ConsoleCommandSender.cs
@@ -0,0 +1,44 @@
+using Microsoft.Extensions.Logging;
+using Obsidian.API;
+using System.Text.RegularExpressions;
+
+namespace Obsidian.GuiConsole.Services.Command;
+
+///
+/// Command sender for console input and output
+///
+/// Temp use
+public class ConsoleCommandSender(ILogger logger): ICommandSender
+{
+ public CommandIssuers Issuer { get; } = CommandIssuers.Console;
+ public IPlayer? Player { get; } = null;
+
+ ///
+ /// Implementation of SendMessageAsync for console logging
+ ///
+ /// data
+ ///
+ public Task SendMessageAsync(ChatMessage message)
+ {
+ List messageParts = new();
+ messageParts.Add(message.Text);
+ foreach (var extra in message.GetExtras())
+ messageParts.Add(extra.Text);
+ //log
+ foreach (var messagePart in messageParts)
+ {
+ if (string.IsNullOrEmpty(messagePart) || messagePart == "\n")
+ {
+ continue;
+ }
+ //remove color codes §
+ var clearMessage = Regex
+ .Replace(messagePart, "§[0-9a-fk-or]", string.Empty, RegexOptions.IgnoreCase);
+ //log to console
+ logger.LogInformation("[Console] {MessagePart}", clearMessage);
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task SendMessageAsync(ChatMessage message, Guid sender) => throw new NotImplementedException();
+}
diff --git a/Obsidian.GuiConsole/Window/ObsidianConsole.cs b/Obsidian.GuiConsole/Window/ObsidianConsole.cs
new file mode 100644
index 000000000..fc164fdc4
--- /dev/null
+++ b/Obsidian.GuiConsole/Window/ObsidianConsole.cs
@@ -0,0 +1,303 @@
+using Microsoft.Extensions.Logging;
+using Obsidian.GuiConsole.Services.Command;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using Terminal.Gui.Drawing;
+using Terminal.Gui.Input;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+using Attribute = Terminal.Gui.Drawing.Attribute;
+
+namespace Obsidian.GuiConsole.Window;
+///
+/// Render a GUI Console for Obsidian Server
+///
+public class ObsidianConsole : Terminal.Gui.Views.Window
+{
+ private readonly ListView logView;
+ private readonly TextField commandInput;
+ private readonly ObservableCollection logEntries;
+ private readonly Dictionary logLevels = new();
+ private readonly List<(LogLevel level, string message)> originalLogs = new(); // 存储原始日志
+ private int maxLineWidth = 0;
+
+ private CommandMiddleware? commandMiddleware;
+
+ public string GuiTitle => $"Obsidian GUI Console for Minecraft {ServerConstants.DefaultProtocol}";
+
+ public ListView LogView => logView;
+
+ ///
+ /// Add Command Middleware to handle command input
+ ///
+ /// Instance
+ public void AddCommandMiddleware(CommandMiddleware? comm)
+ {
+ commandMiddleware = comm;
+ CommandEntered += async (command) =>
+ {
+ if (this.commandMiddleware != null)
+ {
+ AppendLog(LogLevel.Trace, $"> {command}");
+ try
+ {
+ await this.commandMiddleware.ExecuteCommandFromConsoleAsync(command);
+ }
+ catch (Exception e)
+ {
+ AppendLog(LogLevel.Error, $"Error: {e.Message}");
+ }
+ }
+ };
+ }
+
+ ///
+ /// Build the GUI Console Window
+ ///
+ public ObsidianConsole()
+ {
+ Title = GuiTitle;
+
+ // Initial logs source
+ logEntries = new ObservableCollection();
+
+ FrameChanged += (_, s) =>
+ {
+ var newWidth = s.Value.Width - 4;
+ if (newWidth != maxLineWidth && newWidth > 0)
+ {
+ maxLineWidth = newWidth;
+ RewrapAllLogs();
+ }
+ };
+ logView = new ListView
+ {
+ X = 0,
+ Y = 1,
+ Width = Dim.Fill(),
+ Height = Dim.Fill(1),
+ Source = new ListWrapper(logEntries),
+ Arrangement = ViewArrangement.BottomResizable
+ };
+
+ //Add Color Rendering
+ logView.RowRender += OnRowRender;
+
+ //Auto Scroll Handling
+ logEntries.CollectionChanged += OnLogEntriesChanged;
+
+ commandInput = new TextField
+ {
+ X = 2,
+ Y = Pos.AnchorEnd(1),
+ Width = Dim.Fill(),
+ Height = 1
+ };
+
+ var prompt = new Label
+ {
+ X = 0,
+ Y = Pos.AnchorEnd(1),
+ Text = "> ",
+ Width = 2,
+ Height = 1,
+ };
+ prompt.SetScheme(new Scheme()
+ {
+ Normal = new Attribute(Color.DarkGray, Color.Black)
+ });
+
+ commandInput.KeyDown += (_, x) =>
+ {
+ if (x == Key.Enter)
+ {
+ var command = commandInput.Text;
+ OnCommandEntered(command);
+ commandInput.Text = "";
+ x.Handled = true;
+ }
+ };
+
+ Add(prompt);
+ Add(logView);
+ Add(commandInput);
+
+ var windowScheme = GetScheme();
+ var newScheme = new Scheme()
+ {
+ Normal = new Attribute(windowScheme.Normal.Foreground, Color.Black),
+ Focus = new Attribute(windowScheme.Focus.Foreground, windowScheme.Focus.Background),
+ HotNormal = new Attribute(windowScheme.HotNormal.Foreground, windowScheme.HotNormal.Background),
+ HotFocus = new Attribute(windowScheme.HotFocus.Foreground, windowScheme.HotFocus.Background)
+ };
+ SetScheme(newScheme);
+ }
+ ///
+ /// Command Entered Event
+ ///
+ public event Action? CommandEntered;
+ ///
+ /// On Command Entered
+ ///
+ protected virtual void OnCommandEntered(string command)
+ {
+ CommandEntered?.Invoke(command);
+
+ }
+
+ ///
+ /// Row Render Event for Color Coding
+ ///
+ /// nope
+ /// row
+ private void OnRowRender(object? sender, ListViewRowEventArgs e)
+ {
+ if (logLevels.TryGetValue(e.Row, out LogLevel level))
+ {
+ Color color = level switch
+ {
+ LogLevel.Trace or LogLevel.Debug => Color.Gray,
+ LogLevel.Information => Color.Green,
+ LogLevel.Warning => Color.Yellow,
+ LogLevel.Error or LogLevel.Critical => Color.Red,
+ _ => Color.Gray
+ };
+
+ e.RowAttribute = new Attribute(color, Color.Black);
+ }
+ }
+
+ ///
+ /// Auto Scroll to Bottom on New Log Entry
+ ///
+ /// nope
+ /// argument
+ private void OnLogEntriesChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Add)
+ {
+ AutoScrollToBottom();
+ }
+ }
+ ///
+ /// Auto Scroll to Bottom Implementation
+ ///
+ private void AutoScrollToBottom()
+ {
+ if (logEntries.Count > 0)
+ {
+ logView.SelectedItem = logEntries.Count - 1;
+ logView.TopItem = Math.Max(0, logEntries.Count - logView.Viewport.Height);
+ }
+ }
+
+ ///
+ /// Add Log Entry to Console
+ ///
+ /// Log level
+ /// message
+ public void AppendLog(LogLevel level, string message)
+ {
+ //keep original log for re-wrapping
+ originalLogs.Add((level, message));
+
+ var logEntry = $"{DateTime.Now:HH:mm:ss} [{level}] {message}";
+
+ var wrapWidth = maxLineWidth > 0 ? maxLineWidth : 80;
+
+ //shift lines if too long
+ if (logEntry.Length > wrapWidth)
+ {
+ var lines = WrapText(logEntry, wrapWidth);
+ foreach (var line in lines)
+ {
+ logLevels[logEntries.Count] = level;
+ logEntries.Add(line);
+ }
+ }
+ else
+ {
+ logLevels[logEntries.Count] = level;
+ logEntries.Add(logEntry);
+ }
+ }
+
+ ///
+ /// Wrap text into multiple lines with indentation for wrapped lines
+ ///
+ /// text content
+ /// console width
+ ///
+ private List WrapText(string text, int width)
+ {
+ var result = new List();
+
+ if (string.IsNullOrEmpty(text) || width <= 0)
+ {
+ result.Add(text ?? string.Empty);
+ return result;
+ }
+
+ var currentIndex = 0;
+ var isFirstLine = true;
+
+ while (currentIndex < text.Length)
+ {
+ var remainingLength = text.Length - currentIndex;
+ var takeLength = Math.Min(width, remainingLength);
+
+ // add line
+ var line = text.Substring(currentIndex, takeLength);
+ if (!isFirstLine)
+ {
+ line = " " + line.TrimStart();
+ }
+
+ result.Add(line);
+ currentIndex += takeLength;
+ isFirstLine = false;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Rewrap all logs when windows size changed
+ ///
+ private void RewrapAllLogs()
+ {
+ if (originalLogs.Count == 0 || maxLineWidth <= 0)
+ return;
+ var wasAtBottom = logEntries.Count > 0 &&
+ logView.TopItem >= logEntries.Count - logView.Viewport.Height - 5;
+
+ //clear current logs
+ logEntries.Clear();
+ logLevels.Clear();
+
+ //Re-add all logs with new wrapping
+ foreach (var (level, message) in originalLogs)
+ {
+ var logEntry = $"{DateTime.Now:HH:mm:ss} [{level}] {message}";
+
+ if (logEntry.Length > maxLineWidth)
+ {
+ var lines = WrapText(logEntry, maxLineWidth);
+ foreach (var line in lines)
+ {
+ logLevels[logEntries.Count] = level;
+ logEntries.Add(line);
+ }
+ }
+ else
+ {
+ logLevels[logEntries.Count] = level;
+ logEntries.Add(logEntry);
+ }
+ }
+ if (wasAtBottom)
+ {
+ AutoScrollToBottom();
+ }
+ }
+}
diff --git a/Obsidian.GuiConsole/config/server.json b/Obsidian.GuiConsole/config/server.json
new file mode 100644
index 000000000..5b519c857
--- /dev/null
+++ b/Obsidian.GuiConsole/config/server.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "https://raw.githubusercontent.com/ObsidianMC/Obsidian/master/.schema/server.json",
+
+ "allowLan": true,
+ "allowOperatorRequests": true,
+
+ "maxPlayers": 25,
+ "motd": "�k||||�r �5Obsidian �cPre�r-�cRelease �r�k||||�r \n�r�lRunning on .NET �l�c8 �r�l<3",
+
+ "onlineMode": true,
+ "port": 25565,
+ "pregenerateChunkRange": 15,
+ "serverListQuery": "Full",
+ "timeTickSpeedMultiplier": 1,
+ "whitelist": false,
+
+ "network": {
+ "connectionThrottle": 15000,
+ "keepAliveInterval": 10000,
+ "keepAliveTimeoutInterval": 30000,
+ "mulitplayerDebugMode": false
+ },
+
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Obsidian.GuiConsole/config/whitelist.json b/Obsidian.GuiConsole/config/whitelist.json
new file mode 100644
index 000000000..ed3d6011f
--- /dev/null
+++ b/Obsidian.GuiConsole/config/whitelist.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://raw.githubusercontent.com/ObsidianMC/Obsidian/master/.schema/whitelist.json",
+ "whitelistedPlayers": [],
+ "whitelistedIps": []
+}
\ No newline at end of file
diff --git a/Obsidian.slnx b/Obsidian.slnx
index d8b418551..030b1a3ea 100644
--- a/Obsidian.slnx
+++ b/Obsidian.slnx
@@ -1,7 +1,8 @@
-
+
+
diff --git a/Obsidian/Hosting/DependencyInjection.cs b/Obsidian/Hosting/DependencyInjection.cs
index 527dcfffb..0398a8315 100644
--- a/Obsidian/Hosting/DependencyInjection.cs
+++ b/Obsidian/Hosting/DependencyInjection.cs
@@ -114,4 +114,75 @@ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostAppli
return builder;
}
+ ///
+ /// Add GUI Console Support via Terminal.GUI
+ ///
+ ///
+ /// Add Terminal Logger
+ ///
+ public static IHostApplicationBuilder AddObsidianWithGui(this IHostApplicationBuilder builder,Func loggingAdd)
+ {
+ // filename with date,time
+ var logFile = $"logs/{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log";
+ var logFileStream = new FileStream(logFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
+
+ builder.Logging.AddOpenTelemetry(x =>
+ {
+ x.IncludeScopes = true;
+ x.IncludeFormattedMessage = true;
+ });
+ builder.Services.AddLogging(loggingBuilder =>
+ {
+ loggingBuilder.ClearProviders();
+ loggingAdd(loggingBuilder);
+ loggingBuilder.AddProvider(new StreamLoggerProvider(logFileStream));
+ });
+
+ builder.Services.Configure(builder.Configuration);
+ builder.Services.Configure(builder.Configuration);
+
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ //builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+
+ builder.Services.AddHttpClient();
+
+ builder.Services.AddHostedService(sp => sp.GetRequiredService());
+ builder.Services.AddHostedService();
+ builder.Services.AddHostedService(sp => sp.GetRequiredService());
+ builder.Services.AddHostedService();
+
+ builder.Services.AddSingleton(x => x.GetRequiredService());
+ builder.Services.AddSingleton(sp => sp.GetRequiredService());
+ builder.Services.AddSingleton(sp => sp.GetRequiredService());
+
+ builder.Services.AddOpenTelemetry()
+ .WithTracing(tracing =>
+ {
+ if(builder.Environment.IsDevelopment())
+ {
+ tracing.SetSampler();
+ }
+
+ //tracing.AddConsoleExporter();
+ tracing.AddHttpClientInstrumentation();
+ })
+ .WithMetrics(metrics =>
+ {
+ //metrics.AddConsoleExporter();
+
+ metrics.AddRuntimeInstrumentation().AddMeter("Obsidian.Server", "Obsidian.Client", "System.Net.Http");
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ builder.Services.AddSingleton();
+ return builder;
+ }
+
}