โ๏ธ Configuration & Logging
Comprehensive guide to configuring WebSpark.HttpClientUtility with advanced logging and monitoring options
๐ appsettings.json Configuration
Complete configuration example with all available options:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"WebSpark.HttpClientUtility": "Debug"
}
},
// cURL Command Logging Configuration
"CsvOutputFolder": "C:\\Logs\\CurlCommands",
"CsvFileName": "http_requests",
"CurlCommandSaver": {
"UseBatchProcessing": true,
"BatchSize": 100,
"BatchFlushIntervalMs": 5000,
"MaxFileSize": 10485760, // 10MB
"MaxRetries": 3,
"RetryDelayMs": 200,
"SanitizeSensitiveInfo": true,
"SensitiveHeaders": [
"Authorization",
"Api-Key",
"X-Api-Key",
"Password",
"Token",
"Cookie"
]
},
// HTTP Client Configuration
"HttpClient": {
"Timeout": "00:00:30", // 30 seconds
"MaxRetries": 3,
"RetryDelayMs": 1000
},
// Polly Resilience Configuration
"PollyOptions": {
"MaxRetryAttempts": 3,
"RetryDelaySeconds": 1,
"RetryStrategy": "Exponential",
"EnableCircuitBreaker": true,
"CircuitBreakerThreshold": 5,
"CircuitBreakerDurationSeconds": 30
},
// Cache Configuration
"CacheOptions": {
"DefaultDurationMinutes": 5,
"MaxCacheSizeMB": 100
},
// OpenTelemetry Configuration
"OpenTelemetry": {
"ServiceName": "WebSpark.HttpClientUtility.Demo",
"ServiceVersion": "1.3.0",
"ConsoleExporter": true,
"JaegerExporter": false,
"JaegerEndpoint": "http://localhost:14268",
"OtlpExporter": false,
"OtlpEndpoint": "http://localhost:4317"
}
}
๐ Complete Dependency Injection Setup
Full Program.cs configuration with all features enabled:
using Microsoft.Extensions.Caching.Memory;
using WebSpark.HttpClientUtility.ClientService;
using WebSpark.HttpClientUtility.RequestResult;
using WebSpark.HttpClientUtility.StringConverter;
using WebSpark.HttpClientUtility.CurlService;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// === Core Services ===
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
builder.Services.AddControllersWithViews();
// === JSON Serialization ===
builder.Services.AddSingleton<IStringConverter, SystemJsonStringConverter>();
// Or use Newtonsoft.Json:
// builder.Services.AddSingleton<IStringConverter, NewtonsoftJsonStringConverter>();
// === Basic HTTP Client Service ===
builder.Services.AddScoped<IHttpClientService, HttpClientService>();
// === HttpRequestResult Service with Full Decorator Chain ===
builder.Services.AddScoped<HttpRequestResultService>(); // Base service
builder.Services.AddScoped<IHttpRequestResultService>(provider =>
{
// Start with base service
IHttpRequestResultService service =
provider.GetRequiredService<HttpRequestResultService>();
// Layer 1: Add Caching (optional)
if (builder.Configuration.GetValue<bool>("Features:EnableCaching", true))
{
service = new HttpRequestResultServiceCache(
provider.GetRequiredService<ILogger<HttpRequestResultServiceCache>>(),
service,
provider.GetRequiredService<IMemoryCache>()
);
}
// Layer 2: Add Polly Resilience (optional)
if (builder.Configuration.GetValue<bool>("Features:EnablePolly", true))
{
var pollyOptions = new HttpRequestResultPollyOptions
{
MaxRetryAttempts = builder.Configuration.GetValue<int>("PollyOptions:MaxRetryAttempts", 3),
RetryDelay = TimeSpan.FromSeconds(
builder.Configuration.GetValue<int>("PollyOptions:RetryDelaySeconds", 1)),
EnableCircuitBreaker = builder.Configuration.GetValue<bool>(
"PollyOptions:EnableCircuitBreaker", true),
CircuitBreakerThreshold = builder.Configuration.GetValue<int>(
"PollyOptions:CircuitBreakerThreshold", 5),
CircuitBreakerDuration = TimeSpan.FromSeconds(
builder.Configuration.GetValue<int>("PollyOptions:CircuitBreakerDurationSeconds", 30))
};
service = new HttpRequestResultServicePolly(
provider.GetRequiredService<ILogger<HttpRequestResultServicePolly>>(),
service,
pollyOptions
);
}
// Layer 3: Add Telemetry (outermost layer - always enabled)
service = new HttpRequestResultServiceTelemetry(
provider.GetRequiredService<ILogger<HttpRequestResultServiceTelemetry>>(),
service
);
return service;
});
// === cURL Command Logging (optional) ===
if (!string.IsNullOrWhiteSpace(builder.Configuration["CsvOutputFolder"]))
{
builder.Services.AddSingleton<CurlCommandSaver>(provider =>
new CurlCommandSaver(
provider.GetRequiredService<ILogger<CurlCommandSaver>>(),
builder.Configuration
)
);
}
// === OpenTelemetry (optional) ===
if (builder.Configuration.GetValue<bool>("OpenTelemetry:Enabled", false))
{
builder.Services.AddOpenTelemetry()
.WithTracing(tracerBuilder =>
{
tracerBuilder
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService(
builder.Configuration["OpenTelemetry:ServiceName"] ?? "MyApp",
builder.Configuration["OpenTelemetry:ServiceVersion"] ?? "1.0.0"))
.AddSource("WebSpark.HttpClientUtility")
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation();
if (builder.Configuration.GetValue<bool>("OpenTelemetry:ConsoleExporter", false))
{
tracerBuilder.AddConsoleExporter();
}
if (builder.Configuration.GetValue<bool>("OpenTelemetry:JaegerExporter", false))
{
tracerBuilder.AddJaegerExporter(options =>
{
options.AgentHost = builder.Configuration["OpenTelemetry:JaegerHost"] ?? "localhost";
options.AgentPort = builder.Configuration.GetValue<int>("OpenTelemetry:JaegerPort", 6831);
});
}
});
}
var app = builder.Build();
// Configure middleware...
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
๐ Logging Configuration
Log Levels Explained
| Level | Use Case | Performance Impact | Recommended For |
|---|---|---|---|
Trace |
Extremely detailed debugging | High | Development only |
Debug |
Detailed diagnostic information | Medium-High | Development, troubleshooting |
Information |
General flow of application | Low | Production (recommended) |
Warning |
Unusual but handled situations | Very Low | Production (critical services) |
Error |
Errors and exceptions | Minimal | Always enabled |
Critical |
Critical failures | Minimal | Always enabled |
Example Logging Configuration
{
"Logging": {
"LogLevel": {
// Default for all namespaces
"Default": "Information",
// Reduce noise from Microsoft libraries
"Microsoft": "Warning",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
// Enable detailed logging for HttpClientUtility
"WebSpark.HttpClientUtility": "Debug",
"WebSpark.HttpClientUtility.RequestResult": "Debug",
"WebSpark.HttpClientUtility.ClientService": "Information",
"WebSpark.HttpClientUtility.CurlService": "Information",
// External services
"System.Net.Http.HttpClient": "Warning"
},
// File logging provider (optional - requires Serilog or similar)
"File": {
"Path": "Logs/app-.log",
"RollingInterval": "Day",
"RetainedFileCountLimit": 30,
"OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"
}
}
}
Structured Logging Example
// All HttpClientUtility logs include structured data:
// - CorrelationId: Unique request identifier
// - RequestPath: URL being requested
// - RequestMethod: HTTP method (GET, POST, etc.)
// - Duration: Request duration in milliseconds
// - StatusCode: HTTP status code
// - CallerMemberName: Method that made the request
// - CallerFilePath: File where request originated
// - CallerLineNumber: Line number of the request
// Example log output:
// [2025-01-08 10:30:45.123 -05:00] [INF] WebSpark.HttpClientUtility.RequestResult.HttpRequestResultService
// HTTP Request: GET https://v2.jokeapi.dev/joke/Programming?safe-mode
// CorrelationId: a1b2c3d4-e5f6-7890-abcd-ef1234567890
// Duration: 245ms
// StatusCode: 200
// CallerMemberName: GetJokeAsync
// CallerFilePath: C:\Projects\MyApp\Services\JokeService.cs
// CallerLineNumber: 42
๐ cURL Command Logging Configuration
Note: cURL command logging is optional. If
CsvOutputFolder
is not configured, the feature is automatically disabled with no impact on application performance.
Configuration Options
{
// === Required (Feature is disabled if not set) ===
"CsvOutputFolder": "C:\\Logs\\CurlCommands", // Where to save CSV files
// === Optional Settings ===
"CsvFileName": "http_requests", // Base filename (default: "curl_commands")
"CurlCommandSaver": {
// Batch Processing (improves performance for high-volume scenarios)
"UseBatchProcessing": true, // Enable batch mode (default: true)
"BatchSize": 100, // Records per batch (default: 100)
"BatchFlushIntervalMs": 5000, // Flush interval in ms (default: 5000)
// File Management
"MaxFileSize": 10485760, // 10MB - rotate when reached (default: 10MB)
"MaxRetries": 3, // Retry attempts for file operations (default: 3)
"RetryDelayMs": 200, // Delay between retries (default: 200ms)
// Security & Privacy
"SanitizeSensitiveInfo": true, // Redact sensitive data (default: true)
"SensitiveHeaders": [ // Headers to redact
"Authorization",
"Api-Key",
"X-Api-Key",
"Password",
"Token",
"Cookie",
"X-Auth-Token",
"Access-Token"
]
}
}
CSV Output Format
Each HTTP request is logged as a row with the following columns:
| Column | Description | Example |
|---|---|---|
Timestamp |
When the request was made | 2025-01-08T10:30:45.1234567Z |
CurlCommand |
Complete cURL command | curl -X POST "https://api.example.com/data" -H "Content-Type: application/json" -d '{"id":1}' |
RequestPath |
Full URL | https://api.example.com/data |
RequestMethod |
HTTP method | POST |
CallingMethod |
Method that made the request | ProcessDataAsync |
CallingFile |
Source file path | C:\Projects\MyApp\Services\DataService.cs |
CallingLineNumber |
Line number in source file | 142 |
File Rotation
When a CSV file reaches MaxFileSize, it's automatically rotated:
// Original file
http_requests.csv
// After rotation (timestamp appended)
http_requests_20250108_103045.csv
http_requests.csv โ New file starts
Performance Impact
Minimal Performance Impact
- Batch Processing: Records queued in memory, written in batches
- Async Operations: File I/O doesn't block request threads
- Automatic Throttling: Batch size limits memory usage
- Error Isolation: File logging errors don't affect application
Disabling cURL Logging
To disable, simply remove or comment out the CsvOutputFolder setting:
{
// "CsvOutputFolder": "C:\\Logs\\CurlCommands", โ Commented out or removed
// The application will log a warning and continue without file logging:
// [WARN] CsvOutputFolder is not configured. cURL commands will not be saved to file.
}
๐ OpenTelemetry Configuration
For comprehensive distributed tracing and observability:
// Install required packages:
// dotnet add package OpenTelemetry.Extensions.Hosting
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// dotnet add package OpenTelemetry.Instrumentation.Http
// dotnet add package OpenTelemetry.Exporter.Console // For console output
// dotnet add package OpenTelemetry.Exporter.Jaeger // For Jaeger
// dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol // For OTLP
builder.Services.AddOpenTelemetry()
.WithTracing(tracerBuilder =>
{
tracerBuilder
// Set service information
.SetResourceBuilder(ResourceBuilder.CreateDefault()
.AddService("MyApplication", "1.0.0"))
// Add instrumentation
.AddSource("WebSpark.HttpClientUtility")
.AddHttpClientInstrumentation(options =>
{
// Enrich with additional data
options.EnrichWithHttpRequestMessage = (activity, request) =>
{
activity.SetTag("http.request.header.correlation_id",
request.Headers.GetValues("X-Correlation-ID").FirstOrDefault());
};
options.EnrichWithHttpResponseMessage = (activity, response) =>
{
activity.SetTag("http.response.status_code", (int)response.StatusCode);
};
})
.AddAspNetCoreInstrumentation()
// Choose exporters
.AddConsoleExporter() // Development
.AddJaegerExporter(options => // Production
{
options.AgentHost = "localhost";
options.AgentPort = 6831;
})
.AddOtlpExporter(options => // OTLP-compatible backends (Grafana, etc.)
{
options.Endpoint = new Uri("http://localhost:4317");
});
});
๐ Understanding the Decorator Chain
The decorator pattern allows you to add features without modifying core code:
Order Matters!
The recommended order is: Base โ Cache โ Polly โ Telemetry
| Layer | Purpose | Position | Why This Order? |
|---|---|---|---|
| Base Service | Core HTTP functionality | Innermost (1st) | The foundation that actually makes HTTP calls |
| Cache | Response caching | 2nd | Check cache before retrying or measuring |
| Polly | Retries & circuit breaker | 3rd | Retry failed requests, but use cache if available |
| Telemetry | Monitoring & tracing | Outermost (4th) | Measure total time including retries and caching |
Common Mistake: Putting Telemetry before Cache means cache hits won't be measured correctly!
๐ Environment-Specific Configuration
Use different settings for Development, Staging, and Production:
File Structure
appsettings.json โ Base settings (all environments)
appsettings.Development.json โ Development overrides
appsettings.Staging.json โ Staging overrides
appsettings.Production.json โ Production overrides
appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"WebSpark.HttpClientUtility": "Trace" // Maximum detail for development
}
},
"CsvOutputFolder": "C:\\Dev\\Logs", // Local dev folder
"CurlCommandSaver": {
"UseBatchProcessing": false, // Immediate writes for debugging
"SanitizeSensitiveInfo": false // Full data in development
},
"OpenTelemetry": {
"ConsoleExporter": true, // Console output for easy viewing
"JaegerExporter": false
}
}
appsettings.Production.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"WebSpark.HttpClientUtility": "Information" // Less noise in production
}
},
"CsvOutputFolder": "/var/log/myapp/curl", // Production log path
"CurlCommandSaver": {
"UseBatchProcessing": true, // Better performance
"BatchSize": 1000, // Larger batches
"SanitizeSensitiveInfo": true // Always sanitize in production!
},
"OpenTelemetry": {
"ConsoleExporter": false,
"JaegerExporter": true, // Send to Jaeger/Grafana
"OtlpExporter": true
}
}
โ Configuration Best Practices
โ Do
- Use Information log level in production
- Always enable SanitizeSensitiveInfo in production
- Use batch processing for high-volume scenarios
- Configure file rotation to prevent disk space issues
- Use environment-specific configuration files
- Store secrets in Azure Key Vault or environment variables
- Enable structured logging for better querying
- Set appropriate log retention policies
- Use correlation IDs to trace requests
- Monitor log file sizes and disk usage
โ Don't
- Use Trace or Debug in production
- Log sensitive data (passwords, tokens) unredacted
- Disable SanitizeSensitiveInfo in production
- Store credentials in appsettings.json
- Ignore log rotation - fills disk!
- Use synchronous file I/O for logging
- Log every single HTTP request in high-traffic apps
- Forget to test logging configuration in lower environments
- Mix development and production configurations
- Commit appsettings.Production.json with secrets
Pro Tips
- Start Simple: Begin with just Information logging and add detail as needed
- Use Correlation IDs: Every request automatically gets a unique ID for tracking
- Monitor Performance: Watch for logging overhead in high-traffic scenarios
- Archive Logs: Move old log files to cold storage or delete after retention period
- Test Configuration: Validate logging works in each environment before deploying