Skip to content

Commit affa7fc

Browse files
authored
Add structured logging with mcpd-compatible log format (#10)
1 parent e889a5b commit affa7fc

File tree

4 files changed

+107
-27
lines changed

4 files changed

+107
-27
lines changed

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,15 @@ public override Task<HTTPResponse> HandleRequest(HTTPRequest request, Grpc.Core.
132132

133133
### Logging
134134

135-
Plugins that extend `BasePlugin` have access to an `ILogger` instance via the `Logger` property. This allows plugins to emit structured logs that appear in the host application's output.
135+
Plugins that extend `BasePlugin` have access to an `ILogger` instance via the `Logger` property. The SDK automatically provides a default logger that outputs logs in a format compatible with mcpd's log level inference:
136+
137+
```
138+
[INFO] Plugin server listening on unix /var/...
139+
[WARN] Rate limit exceeded
140+
[ERROR] Failed to process request
141+
```
142+
143+
This format allows mcpd to properly categorize log messages by severity.
136144

137145
```csharp
138146
using Microsoft.Extensions.Logging;
@@ -142,13 +150,13 @@ public class MyPlugin : BasePlugin
142150
{
143151
public override Task<Empty> Configure(PluginConfig request, Grpc.Core.ServerCallContext context)
144152
{
145-
Logger?.LogInformation("Plugin configured with {Count} settings", request.Settings.Count);
153+
Logger.LogInformation("Plugin configured with {Count} settings", request.Settings.Count);
146154
return Task.FromResult(new Empty());
147155
}
148156

149157
public override Task<HTTPResponse> HandleRequest(HTTPRequest request, Grpc.Core.ServerCallContext context)
150158
{
151-
Logger?.LogInformation("Processing request: {Method} {Path}", request.Method, request.Path);
159+
Logger.LogInformation("Processing request: {Method} {Path}", request.Method, request.Path);
152160

153161
// Your plugin logic here.
154162
@@ -157,7 +165,20 @@ public class MyPlugin : BasePlugin
157165
}
158166
```
159167

160-
The SDK automatically suppresses ASP.NET Core framework logs at the Info level to reduce noise. Plugin logs at Info level and above will appear normally in the host application's output.
168+
For advanced scenarios, you can provide your own logger:
169+
170+
```csharp
171+
var loggerFactory = LoggerFactory.Create(builder =>
172+
{
173+
builder.AddJsonConsole();
174+
builder.SetMinimumLevel(LogLevel.Debug);
175+
});
176+
var logger = loggerFactory.CreateLogger<MyPlugin>();
177+
178+
return await PluginServer.Serve<MyPlugin>(args, logger);
179+
```
180+
181+
The SDK automatically suppresses ASP.NET Core framework logs at the Info level to reduce noise.
161182

162183
## Running Your Plugin
163184

src/MozillaAI.Mcpd.Plugins.Sdk/BasePlugin.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Google.Protobuf.WellKnownTypes;
22
using Grpc.Core;
33
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Logging.Abstractions;
45

56
namespace MozillaAI.Mcpd.Plugins.V1;
67

@@ -46,9 +47,9 @@ namespace MozillaAI.Mcpd.Plugins.V1;
4647
public class BasePlugin : Plugin.PluginBase
4748
{
4849
/// <summary>
49-
/// Logger instance for the plugin. Available after SetLogger is called by the SDK.
50+
/// Logger instance for the plugin.
5051
/// </summary>
51-
protected ILogger? Logger { get; private set; }
52+
protected ILogger Logger { get; private set; } = NullLogger.Instance;
5253

5354
/// <summary>
5455
/// SetLogger is called by the SDK to provide a logger to the plugin.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Logging.Abstractions;
3+
using Microsoft.Extensions.Logging.Console;
4+
5+
namespace MozillaAI.Mcpd.Plugins.V1;
6+
7+
/// <summary>
8+
/// Console formatter that outputs log messages in a format compatible with mcpd's log inference.
9+
/// </summary>
10+
internal sealed class McpdLogFormatter : ConsoleFormatter
11+
{
12+
public McpdLogFormatter() : base("mcpd")
13+
{
14+
}
15+
16+
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
17+
{
18+
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
19+
if (string.IsNullOrEmpty(message))
20+
{
21+
return;
22+
}
23+
24+
var levelString = GetLevelString(logEntry.LogLevel);
25+
textWriter.Write(levelString);
26+
textWriter.Write(' ');
27+
textWriter.WriteLine(message);
28+
29+
if (logEntry.Exception != null)
30+
{
31+
textWriter.WriteLine(logEntry.Exception.ToString());
32+
}
33+
}
34+
35+
private static string GetLevelString(LogLevel logLevel)
36+
{
37+
return logLevel switch
38+
{
39+
LogLevel.Trace => "[TRACE]",
40+
LogLevel.Debug => "[DEBUG]",
41+
LogLevel.Information => "[INFO]",
42+
LogLevel.Warning => "[WARN]",
43+
LogLevel.Error => "[ERROR]",
44+
LogLevel.Critical => "[ERROR]",
45+
_ => "[INFO]"
46+
};
47+
}
48+
}

src/MozillaAI.Mcpd.Plugins.Sdk/PluginServer.cs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
using Microsoft.AspNetCore.Hosting;
55
using Microsoft.AspNetCore.Server.Kestrel.Core;
66
using Microsoft.Extensions.DependencyInjection;
7-
using Microsoft.Extensions.Hosting;
87
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Console;
99

1010
namespace MozillaAI.Mcpd.Plugins.V1;
1111

@@ -31,20 +31,38 @@ public static class PluginServer
3131
/// </summary>
3232
/// <typeparam name="T">The plugin implementation type that inherits from Plugin.PluginBase.</typeparam>
3333
/// <param name="args">Command-line arguments.</param>
34+
/// <param name="logger">Optional logger instance. If null, a default console logger is created.</param>
3435
/// <returns>Exit code (0 for success, 1 for error).</returns>
35-
public static async Task<int> Serve<T>(string[] args) where T : Plugin.PluginBase, new()
36+
public static async Task<int> Serve<T>(string[] args, ILogger? logger = null) where T : Plugin.PluginBase, new()
3637
{
37-
return await Serve(new T(), args);
38+
return await Serve(new T(), args, logger);
3839
}
3940

4041
/// <summary>
4142
/// Starts a gRPC server for the specified plugin instance.
4243
/// </summary>
4344
/// <param name="implementation">The plugin implementation instance.</param>
4445
/// <param name="args">Command-line arguments.</param>
46+
/// <param name="logger">Optional logger instance. If null, a default console logger is created.</param>
4547
/// <returns>Exit code (0 for success, 1 for error).</returns>
46-
public static async Task<int> Serve(Plugin.PluginBase implementation, string[] args)
48+
public static async Task<int> Serve(Plugin.PluginBase implementation, string[] args, ILogger? logger = null)
4749
{
50+
// Create default console logger if none provided.
51+
logger ??= LoggerFactory.Create(builder =>
52+
{
53+
builder.AddConsole(options => options.FormatterName = "mcpd");
54+
builder.AddConsoleFormatter<McpdLogFormatter, ConsoleFormatterOptions>();
55+
builder.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
56+
builder.AddFilter("Microsoft.Extensions", LogLevel.Warning);
57+
builder.AddFilter("Microsoft.Hosting", LogLevel.Warning);
58+
}).CreateLogger(implementation.GetType());
59+
60+
// Provide logger to BasePlugin implementations.
61+
if (implementation is BasePlugin basePlugin)
62+
{
63+
basePlugin.SetLogger(logger);
64+
}
65+
4866
var addressOption = new Option<string>(
4967
name: "--address",
5068
description: "gRPC address (socket path for unix, host:port for tcp)")
@@ -63,27 +81,25 @@ public static async Task<int> Serve(Plugin.PluginBase implementation, string[] a
6381
networkOption
6482
};
6583

66-
rootCommand.SetHandler(async (address, network) =>
67-
{
68-
await RunServer(implementation, address, network);
69-
}, addressOption, networkOption);
84+
rootCommand.SetHandler(async (address, network) => await RunServer(implementation, address, network, logger), addressOption, networkOption);
7085

7186
return await rootCommand.InvokeAsync(args);
7287
}
7388

74-
private static async Task RunServer(Plugin.PluginBase implementation, string address, string network)
89+
private static async Task RunServer(Plugin.PluginBase implementation, string address, string network, ILogger logger)
7590
{
7691
var builder = WebApplication.CreateSlimBuilder();
7792

78-
// Suppress ASP.NET Core diagnostic logs but allow plugin logs.
93+
// Suppress ASP.NET Core diagnostic logs.
7994
builder.Logging.ClearProviders();
80-
builder.Logging.AddConsole();
95+
builder.Logging.AddConsole(options => options.FormatterName = "mcpd");
96+
builder.Logging.AddConsoleFormatter<McpdLogFormatter, ConsoleFormatterOptions>();
8197
builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
8298
builder.Logging.AddFilter("Microsoft.Extensions", LogLevel.Warning);
8399
builder.Logging.AddFilter("Microsoft.Hosting", LogLevel.Warning);
84100

85101
// Configure Kestrel.
86-
builder.WebHost.ConfigureKestrel((context, serverOptions) =>
102+
builder.WebHost.ConfigureKestrel((_, serverOptions) =>
87103
{
88104
if (network.Equals("unix", StringComparison.OrdinalIgnoreCase))
89105
{
@@ -129,7 +145,8 @@ private static async Task RunServer(Plugin.PluginBase implementation, string add
129145
// Map gRPC service.
130146
app.MapGrpcService<PluginServiceAdapter>();
131147

132-
Console.WriteLine($"Plugin server listening on {network} {address}");
148+
logger.LogInformation("Plugin server listening on {Network} {Address}", network, address);
149+
133150

134151
try
135152
{
@@ -152,16 +169,9 @@ private class PluginServiceAdapter : Plugin.PluginBase
152169
{
153170
private readonly Plugin.PluginBase _implementation;
154171

155-
public PluginServiceAdapter(Plugin.PluginBase implementation, ILoggerFactory loggerFactory)
172+
public PluginServiceAdapter(Plugin.PluginBase implementation)
156173
{
157174
_implementation = implementation;
158-
159-
// Provide logger to BasePlugin implementations.
160-
if (_implementation is BasePlugin basePlugin)
161-
{
162-
var logger = loggerFactory.CreateLogger(_implementation.GetType());
163-
basePlugin.SetLogger(logger);
164-
}
165175
}
166176

167177
public override Task<Google.Protobuf.WellKnownTypes.Empty> Configure(PluginConfig request, Grpc.Core.ServerCallContext context)

0 commit comments

Comments
 (0)