Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs
Submodule docs updated from 09132c to 03362c
8 changes: 8 additions & 0 deletions src/Core/Config/AppConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Text.Json.Serialization;
using KernelMemory.Core.Config.Cache;
using KernelMemory.Core.Config.Validation;
using KernelMemory.Core.Logging;

namespace KernelMemory.Core.Config;

Expand Down Expand Up @@ -36,6 +37,13 @@ public sealed class AppConfig : IValidatable
[JsonPropertyName("search")]
public SearchConfig? Search { get; set; }

/// <summary>
/// Logging configuration settings
/// Controls log level, file output, and format
/// </summary>
[JsonPropertyName("logging")]
public LoggingConfig? Logging { get; set; }

/// <summary>
/// Validates the entire configuration tree
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Core/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
<PackageReference Include="cuid.net" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Parlot" />
<!-- Logging - Serilog -->
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Enrichers.Span" />
<PackageReference Include="Serilog.Formatting.Compact" />
</ItemGroup>

<ItemGroup>
Expand Down
52 changes: 52 additions & 0 deletions src/Core/Logging/ActivityEnricher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics;
using Serilog.Core;
using Serilog.Events;

namespace KernelMemory.Core.Logging;

/// <summary>
/// Serilog enricher that adds correlation IDs from System.Diagnostics.Activity.
/// Provides TraceId and SpanId properties for distributed tracing and log correlation.
/// </summary>
public sealed class ActivityEnricher : ILogEventEnricher
{
/// <summary>
/// Property name for the trace ID (from Activity.TraceId).
/// </summary>
public const string TraceIdPropertyName = "TraceId";

/// <summary>
/// Property name for the span ID (from Activity.SpanId).
/// </summary>
public const string SpanIdPropertyName = "SpanId";

/// <summary>
/// Enriches the log event with Activity correlation IDs if available.
/// </summary>
/// <param name="logEvent">The log event to enrich.</param>
/// <param name="propertyFactory">Factory for creating log event properties.</param>
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var activity = Activity.Current;
if (activity == null)
{
return;
}

// Add TraceId for correlating logs across the entire operation
var traceId = activity.TraceId.ToString();
if (!string.IsNullOrEmpty(traceId) && traceId != LoggingConstants.EmptyTraceId)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(TraceIdPropertyName, traceId));
}

// Add SpanId for correlating logs within a specific span
var spanId = activity.SpanId.ToString();
if (!string.IsNullOrEmpty(spanId) && spanId != LoggingConstants.EmptySpanId)
{
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty(SpanIdPropertyName, spanId));
}
}
}
62 changes: 62 additions & 0 deletions src/Core/Logging/EnvironmentDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) Microsoft. All rights reserved.

namespace KernelMemory.Core.Logging;

/// <summary>
/// Detects the current runtime environment from environment variables.
/// Environment detection is critical for security decisions like sensitive data scrubbing.
/// </summary>
public static class EnvironmentDetector
{
/// <summary>
/// Gets the current environment name.
/// Checks DOTNET_ENVIRONMENT first, falls back to ASPNETCORE_ENVIRONMENT,
/// then defaults to Development if neither is set.
/// </summary>
/// <returns>The environment name (e.g., "Development", "Production", "Staging").</returns>
public static string GetEnvironment()
{
// Check DOTNET_ENVIRONMENT first (takes precedence)
var dotNetEnv = Environment.GetEnvironmentVariable(LoggingConstants.DotNetEnvironmentVariable);
if (!string.IsNullOrWhiteSpace(dotNetEnv))
{
return dotNetEnv;
}

// Fall back to ASPNETCORE_ENVIRONMENT
var aspNetEnv = Environment.GetEnvironmentVariable(LoggingConstants.AspNetCoreEnvironmentVariable);
if (!string.IsNullOrWhiteSpace(aspNetEnv))
{
return aspNetEnv;
}

// Default to Development for safety (full logging)
return LoggingConstants.DefaultEnvironment;
}

/// <summary>
/// Checks if the current environment is Production.
/// In Production, sensitive data is scrubbed from logs.
/// </summary>
/// <returns>True if running in Production environment.</returns>
public static bool IsProduction()
{
return string.Equals(
GetEnvironment(),
LoggingConstants.ProductionEnvironment,
StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Checks if the current environment is Development.
/// In Development, full logging is enabled for debugging.
/// </summary>
/// <returns>True if running in Development environment.</returns>
public static bool IsDevelopment()
{
return string.Equals(
GetEnvironment(),
LoggingConstants.DefaultEnvironment,
StringComparison.OrdinalIgnoreCase);
}
}
69 changes: 69 additions & 0 deletions src/Core/Logging/LoggerExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;

namespace KernelMemory.Core.Logging;

/// <summary>
/// Extension methods for ILogger providing CallerMemberName-based method logging.
/// These helpers automatically capture the calling method name for entry/exit logging.
/// </summary>
public static class LoggerExtensions
{
/// <summary>
/// Logs method entry at Debug level with automatic method name capture.
/// Use this at the beginning of public methods for diagnostics.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <param name="param1">Optional first parameter to log.</param>
/// <param name="param2">Optional second parameter to log.</param>
/// <param name="param3">Optional third parameter to log.</param>
/// <param name="methodName">Automatically captured method name (do not pass explicitly).</param>
public static void LogMethodEntry(
this ILogger logger,
object? param1 = null,
object? param2 = null,
object? param3 = null,
[CallerMemberName] string methodName = "")
{
// Skip logging if Debug level is disabled for performance
if (!logger.IsEnabled(LogLevel.Debug))
{
return;
}

// Log with structured parameters for queryability
logger.LogDebug(
"{MethodName} called with {Param1}, {Param2}, {Param3}",
methodName,
param1,
param2,
param3);
}

/// <summary>
/// Logs method exit at Debug level with automatic method name capture.
/// Use this before returning from public methods for diagnostics.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <param name="result">Optional result value to log.</param>
/// <param name="methodName">Automatically captured method name (do not pass explicitly).</param>
public static void LogMethodExit(
this ILogger logger,
object? result = null,
[CallerMemberName] string methodName = "")
{
// Skip logging if Debug level is disabled for performance
if (!logger.IsEnabled(LogLevel.Debug))
{
return;
}

// Log with structured parameters for queryability
logger.LogDebug(
"{MethodName} completed with {Result}",
methodName,
result);
}
}
45 changes: 45 additions & 0 deletions src/Core/Logging/LoggingConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.

using Serilog.Events;

namespace KernelMemory.Core.Logging;

/// <summary>
/// Configuration model for the logging system.
/// Stores log level and file path settings that can be loaded from config files or CLI flags.
/// </summary>
public sealed class LoggingConfig
{
/// <summary>
/// Gets or sets the minimum log level for log output.
/// Defaults to Information which provides useful diagnostics.
/// </summary>
public LogEventLevel Level { get; set; } = LogEventLevel.Information;

/// <summary>
/// Gets or sets the file path for file logging.
/// When null or empty, file logging is disabled.
/// Supports both relative paths (from cwd) and absolute paths.
/// </summary>
public string? FilePath { get; set; }

/// <summary>
/// Gets or sets whether to use JSON format for log output.
/// When false, uses human-readable format (default).
/// JSON format is better for log aggregation systems.
/// </summary>
public bool UseJsonFormat { get; set; }

/// <summary>
/// Gets or sets whether to use async logging.
/// When false (default), uses synchronous logging suitable for CLI.
/// Enable for long-running services to improve performance.
/// </summary>
public bool UseAsyncLogging { get; set; }

/// <summary>
/// Gets a value indicating whether file logging is enabled.
/// Returns true when FilePath is set to a non-empty value.
/// </summary>
public bool IsFileLoggingEnabled => !string.IsNullOrWhiteSpace(this.FilePath);
}
92 changes: 92 additions & 0 deletions src/Core/Logging/LoggingConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft. All rights reserved.

using Serilog.Events;

namespace KernelMemory.Core.Logging;

/// <summary>
/// Centralized constants for the logging system.
/// All magic values related to logging are defined here for maintainability.
/// </summary>
public static class LoggingConstants
{
/// <summary>
/// Default maximum file size before rotation (100MB).
/// Balances history retention with disk usage.
/// </summary>
public const long DefaultFileSizeLimitBytes = 100 * 1024 * 1024;

/// <summary>
/// Default number of log files to retain (30 files).
/// Approximately 1 month of daily logs or ~3GB max storage.
/// </summary>
public const int DefaultRetainedFileCountLimit = 30;

/// <summary>
/// Default minimum log level for file output.
/// Information level provides useful diagnostics without excessive verbosity.
/// </summary>
public const LogEventLevel DefaultFileLogLevel = LogEventLevel.Information;

/// <summary>
/// Default minimum log level for console/stderr output.
/// Only warnings and errors appear on stderr by default.
/// </summary>
public const LogEventLevel DefaultConsoleLogLevel = LogEventLevel.Warning;

/// <summary>
/// Environment variable for .NET runtime environment detection.
/// Takes precedence over ASPNETCORE_ENVIRONMENT.
/// </summary>
public const string DotNetEnvironmentVariable = "DOTNET_ENVIRONMENT";

/// <summary>
/// Fallback environment variable for ASP.NET Core applications.
/// Used when DOTNET_ENVIRONMENT is not set.
/// </summary>
public const string AspNetCoreEnvironmentVariable = "ASPNETCORE_ENVIRONMENT";

/// <summary>
/// Default environment when no environment variable is set.
/// Defaults to Development for developer safety (full logging enabled).
/// </summary>
public const string DefaultEnvironment = "Development";

/// <summary>
/// Production environment name for comparison.
/// Sensitive data is scrubbed only in Production.
/// </summary>
public const string ProductionEnvironment = "Production";

/// <summary>
/// Placeholder text for redacted sensitive data.
/// Used to indicate data was intentionally removed from logs.
/// </summary>
public const string RedactedPlaceholder = "[REDACTED]";

/// <summary>
/// Human-readable output template for log messages.
/// Includes timestamp, level, source context, message, and optional exception.
/// </summary>
public const string HumanReadableOutputTemplate =
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}";

/// <summary>
/// Compact output template for console (stderr) output.
/// Shorter format suitable for CLI error reporting.
/// </summary>
public const string ConsoleOutputTemplate =
"{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}";

/// <summary>
/// Empty trace ID value (32 zeros) used when no Activity is present.
/// Indicates no distributed tracing context is available.
/// </summary>
public const string EmptyTraceId = "00000000000000000000000000000000";

/// <summary>
/// Empty span ID value (16 zeros) used when no Activity is present.
/// Indicates no distributed tracing context is available.
/// </summary>
public const string EmptySpanId = "0000000000000000";
}
Loading
Loading