Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,6 @@ public static void InitializeConfigurationAndOptions(this IServiceCollection ser
.BindConfiguration(string.Empty)
.Configure<IConfiguration, IOptions<ServiceStartOptions>>((options, rootConfiguration, serviceStartOptions) =>
{
// This environment variable can be used to disable telemetry collection entirely. This takes precedence
// over any other settings.
var collectTelemetry = rootConfiguration.GetValue("AZURE_MCP_COLLECT_TELEMETRY", true);
var transport = serviceStartOptions.Value.Transport;
var isStdioTransport = string.IsNullOrEmpty(transport)
|| string.Equals(transport, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase);

// Assembly.GetEntryAssembly is used to retrieve the version of the server application as that is
// the assembly that will run the tool calls.
var entryAssembly = Assembly.GetEntryAssembly();
Expand All @@ -281,6 +274,22 @@ public static void InitializeConfigurationAndOptions(this IServiceCollection ser

options.Version = AssemblyHelper.GetAssemblyVersion(entryAssembly);

// Disable telemetry when support logging is enabled to prevent sensitive data from being sent
// to telemetry endpoints. Support logging captures debug-level information that may contain
// sensitive data, so we disable all telemetry as a safety measure.
if (!string.IsNullOrWhiteSpace(serviceStartOptions.Value.SupportLoggingFolder))
{
options.IsTelemetryEnabled = false;
return;
}

// This environment variable can be used to disable telemetry collection entirely. This takes precedence
// over any other settings.
var collectTelemetry = rootConfiguration.GetValue("AZURE_MCP_COLLECT_TELEMETRY", true);
var transport = serviceStartOptions.Value.Transport;
var isStdioTransport = string.IsNullOrEmpty(transport)
|| string.Equals(transport, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase);

// if transport is not set (default to stdio) or is set to stdio, enable telemetry
// telemetry is disabled for HTTP transport
options.IsTelemetryEnabled = collectTelemetry && isStdioTransport;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Azure.Mcp.Core.Areas.Server.Models;
using Azure.Mcp.Core.Areas.Server.Options;
using Azure.Mcp.Core.Helpers;
using Azure.Mcp.Core.Logging;
using Azure.Mcp.Core.Services.Azure;
using Azure.Mcp.Core.Services.Azure.Authentication;
using Azure.Mcp.Core.Services.Caching;
Expand Down Expand Up @@ -82,6 +83,7 @@ protected override void RegisterOptions(Command command)
command.Options.Add(ServiceOptionDefinitions.DangerouslyDisableHttpIncomingAuth);
command.Options.Add(ServiceOptionDefinitions.InsecureDisableElicitation);
command.Options.Add(ServiceOptionDefinitions.OutgoingAuthStrategy);
command.Options.Add(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir);
command.Validators.Add(commandResult =>
{
string transport = ResolveTransport(commandResult);
Expand All @@ -93,9 +95,42 @@ protected override void RegisterOptions(Command command)
commandResult.GetValueOrDefault<string[]?>(ServiceOptionDefinitions.Tool.Name),
commandResult);
ValidateOutgoingAuthStrategy(commandResult);
ValidateSupportLoggingFolder(commandResult);
});
}

/// <summary>
/// Validates that the support logging folder path is valid when specified.
/// </summary>
/// <param name="commandResult">Command result to update on failure.</param>
private static void ValidateSupportLoggingFolder(CommandResult commandResult)
{
string? folderPath = commandResult.GetValueOrDefault<string?>(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name);

if (folderPath is null)
{
return; // Option not specified, nothing to validate
}

// Validate the folder path is not empty or whitespace
if (string.IsNullOrWhiteSpace(folderPath))
{
commandResult.AddError("The --dangerously-write-support-logs-to-dir option requires a valid folder path.");
return;
}

// Validate the folder path is actually a valid path format
try
{
// GetFullPath will throw for invalid path characters and other path format issues
_ = Path.GetFullPath(folderPath);
}
catch (Exception ex) when (ex is ArgumentException or PathTooLongException or NotSupportedException)
{
commandResult.AddError($"The --dangerously-write-support-logs-to-dir option contains an invalid folder path '{folderPath}': {ex.Message}");
}
}

/// <summary>
/// Binds the parsed command line arguments to the ServiceStartOptions object.
/// </summary>
Expand Down Expand Up @@ -124,7 +159,8 @@ protected override ServiceStartOptions BindOptions(ParseResult parseResult)
Debug = parseResult.GetValueOrDefault<bool>(ServiceOptionDefinitions.Debug.Name),
DangerouslyDisableHttpIncomingAuth = parseResult.GetValueOrDefault<bool>(ServiceOptionDefinitions.DangerouslyDisableHttpIncomingAuth.Name),
InsecureDisableElicitation = parseResult.GetValueOrDefault<bool>(ServiceOptionDefinitions.InsecureDisableElicitation.Name),
OutgoingAuthStrategy = outgoingAuthStrategy
OutgoingAuthStrategy = outgoingAuthStrategy,
SupportLoggingFolder = parseResult.GetValueOrDefault<string?>(ServiceOptionDefinitions.DangerouslyWriteSupportLogsToDir.Name)
};
return options;
}
Expand Down Expand Up @@ -196,6 +232,26 @@ internal static void LogStartTelemetry(ITelemetryService telemetryService, Servi
}
}

/// <summary>
/// Configures support logging when a support logging folder is specified.
/// This enables debug-level logging for troubleshooting and support purposes.
/// </summary>
/// <param name="logging">The logging builder to configure.</param>
/// <param name="options">The server configuration options.</param>
private static void ConfigureSupportLogging(ILoggingBuilder logging, ServiceStartOptions options)
{
if (options.SupportLoggingFolder is null)
{
return;
}

// Set minimum log level to Debug when support logging is enabled
logging.SetMinimumLevel(LogLevel.Debug);

// Add file logging to the specified folder
logging.AddSupportFileLogging(options.SupportLoggingFolder);
}

/// <summary>
/// Validates if the provided mode is a valid mode type.
/// </summary>
Expand Down Expand Up @@ -368,6 +424,8 @@ private IHost CreateStdioHost(ServiceStartOptions serverOptions)
logging.AddFilter("Microsoft.Extensions.Logging.Console.ConsoleLoggerProvider", LogLevel.Debug);
logging.SetMinimumLevel(LogLevel.Debug);
}

ConfigureSupportLogging(logging, serverOptions);
})
.ConfigureServices(services =>
{
Expand All @@ -394,6 +452,7 @@ private IHost CreateHttpHost(ServiceStartOptions serverOptions)
builder.Logging.ConfigureOpenTelemetryLogger();
builder.Logging.AddEventSourceLogger();
builder.Logging.AddConsole();
ConfigureSupportLogging(builder.Logging, serverOptions);

IServiceCollection services = builder.Services;

Expand Down Expand Up @@ -571,6 +630,7 @@ private IHost CreateIncomingAuthDisabledHttpHost(ServiceStartOptions serverOptio
builder.Logging.ConfigureOpenTelemetryLogger();
builder.Logging.AddEventSourceLogger();
builder.Logging.AddConsole();
ConfigureSupportLogging(builder.Logging, serverOptions);

IServiceCollection services = builder.Services;

Expand Down Expand Up @@ -771,6 +831,14 @@ private static WebApplication UseHttpsRedirectionIfEnabled(WebApplication app)
return null;
}

// Disable telemetry when support logging is enabled to prevent sensitive data from being sent
// to telemetry endpoints. Support logging captures debug-level information that may contain
// sensitive data, so we disable all telemetry as a safety measure.
if (!string.IsNullOrWhiteSpace(options.SupportLoggingFolder))
{
return null;
}

string? collectTelemetry = Environment.GetEnvironmentVariable("AZURE_MCP_COLLECT_TELEMETRY");
bool isTelemetryEnabled = string.IsNullOrWhiteSpace(collectTelemetry) ||
(bool.TryParse(collectTelemetry, out bool shouldCollectTelemetry) && shouldCollectTelemetry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class ServiceOptionDefinitions
public const string DangerouslyDisableHttpIncomingAuthName = "dangerously-disable-http-incoming-auth";
public const string InsecureDisableElicitationName = "insecure-disable-elicitation";
public const string OutgoingAuthStrategyName = "outgoing-auth-strategy";
public const string DangerouslyWriteSupportLogsToDirName = "dangerously-write-support-logs-to-dir";

public static readonly Option<string> Transport = new($"--{TransportName}")
{
Expand Down Expand Up @@ -91,4 +92,12 @@ public static class ServiceOptionDefinitions
Description = "Outgoing authentication strategy for Azure service requests. Valid values: NotSet, UseHostingEnvironmentIdentity, UseOnBehalfOf.",
DefaultValueFactory = _ => Options.OutgoingAuthStrategy.NotSet
};

public static readonly Option<string?> DangerouslyWriteSupportLogsToDir = new(
$"--{DangerouslyWriteSupportLogsToDirName}")
{
Required = false,
Description = "Dangerously enables detailed debug-level logging for support and troubleshooting purposes. Specify a folder path where log files will be automatically created with timestamp-based filenames (e.g., azmcp_20251202_143052.log). This may include sensitive information in logs. Use with extreme caution and only when requested by support.",
DefaultValueFactory = _ => null
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,13 @@ public class ServiceStartOptions
/// </summary>
[JsonPropertyName("outgoingAuthStrategy")]
public OutgoingAuthStrategy OutgoingAuthStrategy { get; set; } = OutgoingAuthStrategy.NotSet;

/// <summary>
/// Gets or sets the folder path for support logging.
/// When specified, detailed debug-level logging is enabled and logs are written to
/// automatically generated files in this folder with timestamp-based filenames.
/// Warning: This may include sensitive information in logs.
/// </summary>
[JsonPropertyName("supportLoggingFolder")]
public string? SupportLoggingFolder { get; set; } = null;
}
54 changes: 54 additions & 0 deletions core/Azure.Mcp.Core/src/Logging/FileLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Extensions.Logging;

namespace Azure.Mcp.Core.Logging;

/// <summary>
/// A simple file logger that writes logs to a file for support and troubleshooting purposes.
/// </summary>
internal sealed class FileLogger(string categoryName, FileLoggerProvider provider) : ILogger
{
private readonly string _categoryName = categoryName;
private readonly FileLoggerProvider _provider = provider;

/// <inheritdoc/>
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;

/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;

/// <inheritdoc/>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}

var message = formatter(state, exception);
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consider using something like SeriLog as suggested in this issue? It provides several configurable options including log rotation.

var logLevelString = GetLogLevelString(logLevel);

var logEntry = $"[{timestamp}] [{logLevelString}] [{_categoryName}] {message}";

if (exception != null)
{
logEntry += Environment.NewLine + exception.ToString();
}

_provider.WriteLog(logEntry);
}

private static string GetLogLevelString(LogLevel logLevel) => logLevel switch
{
LogLevel.Trace => "TRCE",
LogLevel.Debug => "DBUG",
LogLevel.Information => "INFO",
LogLevel.Warning => "WARN",
LogLevel.Error => "ERRR",
LogLevel.Critical => "CRIT",
_ => "NONE"
};
}
Loading