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
25 changes: 16 additions & 9 deletions Core/Cosmos.DataTransfer.Core/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,25 @@ public static async Task<int> Main(string[] args)
{
builder.ConfigureAppConfiguration((hostContext, cfg) =>
{
// Keep explicit .NET precedence order:
// appsettings.json -> appsettings.{Environment}.json (for example appsettings.Development.json)
// -> user secrets (only when Environment == Development)
// -> environment variables -> command line.
// This ensures env vars and CLI args override file-based defaults.
cfg.Sources.Clear();

var exeFolder = AppContext.BaseDirectory;
var appsettings = Path.Combine(exeFolder, "appsettings.json");
if (File.Exists(appsettings))
{
cfg.AddJsonFile(appsettings);
}
var appsettingsEnv = Path.Combine(exeFolder, $"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json");
if (File.Exists(appsettingsEnv))
cfg.SetBasePath(exeFolder);
cfg.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false);
cfg.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false);

if (hostContext.HostingEnvironment.IsDevelopment())
{
cfg.AddJsonFile(appsettingsEnv);
cfg.AddUserSecrets<Program>(optional: true);
}
cfg.AddUserSecrets<Program>();

cfg.AddEnvironmentVariables();
cfg.AddCommandLine(args);
}).ConfigureServices((hostContext, services) =>
{
services.AddTransient<IExtensionLoader, ExtensionLoader>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,58 @@ public void CreateClient_WithoutDisableSslValidation_DoesNotLogWarning()
Assert.IsNotNull(client, "CosmosClient should be created");
}

[TestMethod]
public void CreateClient_WithEnableNetHttpLogging_LogsWarning()
{
var loggerMock = new Mock<ILogger>();
var settings = new TestableCosmosSettings
{
ConnectionString = "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
Database = "testDb",
Container = "testContainer",
EnableNetHttpLogging = true
};

var client = CosmosExtensionServices.CreateClient(settings, "TestDisplay", loggerMock.Object);

loggerMock.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(".NET HTTP diagnostic logging is ENABLED")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);

Assert.IsNotNull(client, "CosmosClient should be created");
}

[TestMethod]
public void CreateClient_WithoutEnableNetHttpLogging_DoesNotLogHttpWarning()
{
var loggerMock = new Mock<ILogger>();
var settings = new TestableCosmosSettings
{
ConnectionString = "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
Database = "testDb",
Container = "testContainer",
EnableNetHttpLogging = false
};

var client = CosmosExtensionServices.CreateClient(settings, "TestDisplay", loggerMock.Object);

loggerMock.Verify(
x => x.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(".NET HTTP diagnostic logging is ENABLED")),
It.IsAny<Exception?>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Never);

Assert.IsNotNull(client, "CosmosClient should be created");
}

private class TestableCosmosSettings : CosmosSettingsBase
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Net.Http;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Diagnostics.Tracing;

namespace Cosmos.DataTransfer.CosmosExtension
{
Expand Down Expand Up @@ -36,6 +37,9 @@ public static class CosmosExtensionServices
return new HttpClient(handler);
});

private static readonly object _httpEventListenerLock = new();
private static EventListener? _httpEventListener;

public static CosmosClient CreateClient(CosmosSettingsBase settings, string displayName, ILogger logger, string? sourceDisplayName = null)
{
string userAgentString = CreateUserAgentString(displayName, sourceDisplayName);
Expand Down Expand Up @@ -82,6 +86,12 @@ public static CosmosClient CreateClient(CosmosSettingsBase settings, string disp
logger.LogWarning("SSL certificate validation is DISABLED. This should ONLY be used for development scenarios. Never use in production.");
clientOptions.ServerCertificateCustomValidationCallback = (cert, chain, errors) => true;
}

if (settings.EnableNetHttpLogging)
{
logger.LogWarning(".NET HTTP diagnostic logging is ENABLED for Cosmos connection troubleshooting. Logs are emitted at Debug level and may include endpoint/request details.");
EnsureHttpEventLoggingEnabled(logger);
}

CosmosClient? cosmosClient;
if (settings.UseRbacAuth)
Expand All @@ -107,6 +117,89 @@ public static CosmosClient CreateClient(CosmosSettingsBase settings, string disp
return cosmosClient;
}

private static void EnsureHttpEventLoggingEnabled(ILogger logger)
{
lock (_httpEventListenerLock)
{
_httpEventListener ??= new NetHttpEventListener(logger);
}
}

private sealed class NetHttpEventListener(ILogger logger) : EventListener
{
private readonly ILogger _logger = logger;

protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name == "System.Net.Http" || eventSource.Name == "System.Net.Security")
{
EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All);
}
}

protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (!_logger.IsEnabled(LogLevel.Debug))
{
return;
}

if (eventData.EventSource.Name != "System.Net.Http" && eventData.EventSource.Name != "System.Net.Security")
{
return;
}

if (string.Equals(eventData.EventName, "EventCounters", StringComparison.Ordinal))
{
return;
}

var payload = FormatPayload(eventData);

_logger.LogDebug(".NET HTTP event {Source}:{EventName} ({EventId}) {Payload}",
eventData.EventSource.Name,
eventData.EventName ?? "unknown",
eventData.EventId,
payload);
}

private static string FormatPayload(EventWrittenEventArgs eventData)
{
if (eventData.Payload is null || eventData.Payload.Count == 0)
{
return string.Empty;
}

return string.Join(", ", eventData.Payload.Select((value, index) =>
{
var key = eventData.PayloadNames is not null && index < eventData.PayloadNames.Count
? eventData.PayloadNames[index]
: $"arg{index}";
return $"{key}={FormatPayloadValue(value)}";
}));
}

private static string FormatPayloadValue(object? value)
{
if (value is null)
{
return "null";
}

if (value is string text)
{
return text;
}

if (value is IEnumerable<KeyValuePair<string, object>> keyValuePairs)
{
return string.Join(";", keyValuePairs.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}

return value.ToString() ?? string.Empty;
}
}

private static string CreateUserAgentString(string displayName, string? sourceDisplayName)
{
// based on:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public abstract class CosmosSettingsBase : IValidatableObject
/// </summary>
public bool AllowBulkExecution { get; set; } = false;

/// <summary>
/// Enables .NET HTTP diagnostic event logging for troubleshooting connectivity issues.
/// Logs are emitted at Debug level and may include endpoint and request details.
/// </summary>
public bool EnableNetHttpLogging { get; set; } = false;

public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!UseRbacAuth && string.IsNullOrEmpty(ConnectionString))
Expand Down
28 changes: 28 additions & 0 deletions Extensions/Cosmos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ These properties will be preserved exactly as they appear in the source when mig
| LimitToEndpoint | Restrict client to endpoint (see CosmosClientOptions.LimitToEndpoint) | false |
| DisableSslValidation | Disable SSL certificate validation (for local dev only; not for production) | false |
| AllowBulkExecution | Enable bulk execution for optimized performance. <br>**Warning:** May affect consistency and error handling. | false |
| EnableNetHttpLogging | Enable .NET HTTP diagnostic events for troubleshooting connectivity issues (Debug log level) | false |

Source and sink require settings used to locate and access the Cosmos DB account. This can be done in one of two ways:

Expand Down Expand Up @@ -79,6 +80,32 @@ Source supports the following optional parameters:
- `UseDefaultProxyCredentials` (`false` by default) - When `true`, includes default credentials in the WebProxy request. Use this when connecting through an authenticated proxy that returns [`407 Proxy Authentication Required`](https://learn.microsoft.com/dotnet/api/system.net.webproxy.credentials?view=net-10.0#remarks).
- `UseDefaultCredentials` (`false` by default) - When `true`, configures the underlying HttpClient with default network credentials. Use this when the connection to CosmosDB requires authentication through a proxy.
- `PreAuthenticate` (`false` by default) - When `true`, enables pre-authentication on the HttpClient, which sends credentials with the initial request rather than waiting for a 401/407 challenge. This can save extra round-trips but should only be used when the endpoint is trusted.
- `EnableNetHttpLogging` (`false` by default) - When `true`, enables `.NET` `System.Net.Http` diagnostic events to help investigate connection issues. These events are emitted at Debug level and can include endpoint/request details.

### Connectivity troubleshooting (.NET HTTP diagnostics)

When connection failures are not verbose enough, enable `EnableNetHttpLogging` and run the tool with `Debug` logging.

```json
{
"SourceSettings": {
"ConnectionMode": "Gateway",
"LimitToEndpoint": true,
"DisableSslValidation": true,
"EnableNetHttpLogging": true
}
}
```

Set logging to Debug before running:

- Linux/macOS: `export Logging__LogLevel__Default=Debug`
- Windows (cmd): `set Logging__LogLevel__Default=Debug`
- Windows (PowerShell): `$env:Logging__LogLevel__Default = "Debug"`

This emits `System.Net.Http` and `System.Net.Security` events at Debug level, which helps distinguish TLS/certificate failures from transport/proxy/network failures.
Configuration follows normal .NET precedence, so environment variables override `appsettings.json` values.
Command-line arguments have highest precedence and override both environment variables and `appsettings.json`.

### Always Encrypted

Expand Down Expand Up @@ -175,6 +202,7 @@ For development purposes with SSL validation disabled:
- **`UseDefaultProxyCredentials`**: Optional, defaults to `false`. When `true`, includes default credentials in the WebProxy request. Use this when connecting through an authenticated proxy that returns [`407 Proxy Authentication Required`](https://learn.microsoft.com/dotnet/api/system.net.webproxy.credentials?view=net-10.0#remarks).
- **`UseDefaultCredentials`**: Optional, defaults to `false`. When `true`, configures the underlying HttpClient with default network credentials. Use this when the connection to CosmosDB requires authentication through a proxy.
- **`PreAuthenticate`**: Optional, defaults to `false`. When `true`, enables pre-authentication on the HttpClient, which sends credentials with the initial request rather than waiting for a 401/407 challenge. This can save extra round-trips but should only be used when the endpoint is trusted.
- **`EnableNetHttpLogging`**: Optional, defaults to `false`. When `true`, enables `.NET` `System.Net.Http` diagnostic events for troubleshooting. Events are logged at Debug level and may include endpoint/request details.

- **`LimitToEndpoint`**: Optional, defaults to `false`. When the value of this property is false, the Cosmos DB SDK will automatically discover
write and read regions, and use them when the configured application region is not available.
Expand Down
Loading