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
4 changes: 4 additions & 0 deletions libs/common/Metrics/InfoMetricsType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ public enum InfoMetricsType : byte
/// Scan and return distribution of in-memory portion of hybrid logs for main store and object store
/// </summary>
HLOGSCAN,
/// <summary>
/// Per-command usage statistics (calls, failures, rejections)
/// </summary>
COMMANDSTATS,
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions libs/host/Configuration/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ internal sealed class Options : ICloneable
[Option("latency-monitor", Required = false, HelpText = "Track latency of various events.")]
public bool? LatencyMonitor { get; set; }

[OptionValidation]
[Option("commandstats-monitor", Required = false, HelpText = "Track per-command usage statistics (calls, failures, rejections). Exposed via INFO COMMANDSTATS.")]
public bool? CommandStatsMonitor { get; set; }

[IntRangeValidation(0, int.MaxValue)]
[Option("slowlog-log-slower-than", Required = false, HelpText = "Threshold (microseconds) for logging command in the slow log. 0 to disable.")]
public int SlowLogThreshold { get; set; }
Expand Down Expand Up @@ -916,6 +920,7 @@ endpoint is IPEndPoint listenEp && clusterAnnounceEndpoint[0] is IPEndPoint anno
ServerCertificateRequired.GetValueOrDefault(),
logger: logger) : null,
LatencyMonitor = LatencyMonitor.GetValueOrDefault(),
CommandStatsMonitor = CommandStatsMonitor.GetValueOrDefault(),
SlowLogThreshold = SlowLogThreshold,
SlowLogMaxEntries = SlowLogMaxEntries,
MetricsSamplingFrequency = MetricsSamplingFrequency,
Expand Down
3 changes: 3 additions & 0 deletions libs/host/Configuration/Redis/RedisOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ internal class RedisOptions
[RedisOption("latency-tracking", nameof(Options.LatencyMonitor))]
public Option<RedisBoolean> LatencyTracking { get; set; }

[RedisOption("commandstats-tracking", nameof(Options.CommandStatsMonitor))]
public Option<RedisBoolean> CommandStatsTracking { get; set; }

[RedisOption("loglevel", nameof(Options.LogLevel))]
public Option<RedisLogLevel> LogLevel { get; set; }

Expand Down
3 changes: 3 additions & 0 deletions libs/host/defaults.conf
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@
/* Track latency of various events. */
"LatencyMonitor" : false,

/* Track per-command usage statistics (calls, failures, rejections). Exposed via INFO COMMANDSTATS. */
"CommandStatsMonitor" : false,

/* Threshold (microseconds) for logging command in the slow log. 0 to disable. */
"SlowLogThreshold": 0,

Expand Down
127 changes: 127 additions & 0 deletions libs/server/Metrics/CommandStats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
using System.Runtime.CompilerServices;

namespace Garnet.server
{
/// <summary>
/// Per-command statistics entry tracking calls, failures, and rejections.
/// Follows the Redis COMMANDSTATS convention.
/// </summary>
public struct CommandStatsEntry
{
/// <summary>
/// Total number of times this command was called.
/// </summary>
public ulong Calls;

/// <summary>
/// Total number of times this command failed (returned an error response).
/// </summary>
public ulong FailedCalls;

/// <summary>
/// Total number of times this command was rejected before execution (e.g., ACL denied, OOM).
/// </summary>
public ulong RejectedCalls;
}

/// <summary>
/// Tracks per-command usage statistics for built-in commands.
/// Array-indexed by RespCommand enum value for O(1) access.
/// Each session owns its own instance (single-writer, no locking needed).
/// </summary>
public class CommandStats
{
/// <summary>
/// Number of entries in the stats array, sized to hold all valid RespCommand values.
/// </summary>
internal static readonly int EntryCount = (int)RespCommandExtensions.LastValidCommand + 1;

/// <summary>
/// Per-command statistics entries indexed by (int)RespCommand.
/// </summary>
internal CommandStatsEntry[] entries;

/// <summary>
/// Creates a new CommandStats instance with zeroed entries.
/// </summary>
public CommandStats()
{
entries = new CommandStatsEntry[EntryCount];
}

/// <summary>
/// Increment the calls counter for the given command.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IncrementCalls(RespCommand cmd)
{
ushort idx = (ushort)cmd;
if (idx < entries.Length)
entries[idx].Calls++;
}

/// <summary>
/// Increment the failed calls counter for the given command.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IncrementFailed(RespCommand cmd)
{
ushort idx = (ushort)cmd;
if (idx < entries.Length)
entries[idx].FailedCalls++;
}

/// <summary>
/// Increment the rejected calls counter for the given command.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IncrementRejected(RespCommand cmd)
{
ushort idx = (ushort)cmd;
if (idx < entries.Length)
entries[idx].RejectedCalls++;
}

/// <summary>
/// Get the stats entry for the given command.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public CommandStatsEntry GetEntry(RespCommand cmd)
{
ushort idx = (ushort)cmd;
if (idx < entries.Length)
return entries[idx];
return default;
}

/// <summary>
/// Add another CommandStats instance into this one (for aggregation).
/// </summary>
internal void Add(CommandStats other)
{
if (other?.entries == null)
return;

int len = Math.Min(entries.Length, other.entries.Length);
for (int i = 0; i < len; i++)
{
entries[i].Calls += other.entries[i].Calls;
entries[i].FailedCalls += other.entries[i].FailedCalls;
entries[i].RejectedCalls += other.entries[i].RejectedCalls;
}
}

/// <summary>
/// Reset all entries to zero.
/// </summary>
internal void Reset()
{
Array.Clear(entries, 0, entries.Length);
}

}
}
15 changes: 14 additions & 1 deletion libs/server/Metrics/GarnetServerMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@ internal struct GarnetServerMetrics
/// </summary>
public readonly GarnetLatencyMetrics globalLatencyMetrics;

public GarnetServerMetrics(bool trackStats, bool trackLatency, GarnetServerMonitor monitor)
/// <summary>
/// Global per-command usage statistics (calls, failures, rejections).
/// </summary>
public CommandStats globalCommandStats;

/// <summary>
/// History of per-command usage statistics from disposed sessions.
/// </summary>
public CommandStats historyCommandStats;

public GarnetServerMetrics(bool trackStats, bool trackLatency, bool trackCommandStats, GarnetServerMonitor monitor)
{
total_connections_received = 0;
total_connections_disposed = 0;
Expand All @@ -49,6 +59,9 @@ public GarnetServerMetrics(bool trackStats, bool trackLatency, GarnetServerMonit
historySessionMetrics = trackStats ? new GarnetSessionMetrics() : null;

globalLatencyMetrics = trackLatency ? new() : null;

globalCommandStats = trackCommandStats ? new CommandStats() : null;
historyCommandStats = trackCommandStats ? new CommandStats() : null;
}

public void Dispose()
Expand Down
58 changes: 53 additions & 5 deletions libs/server/Metrics/GarnetServerMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal enum EventType : byte
internal sealed class GarnetServerMonitor
{
public readonly Dictionary<InfoMetricsType, bool>
resetEventFlags = GarnetInfoMetrics.DefaultInfo.ToDictionary(x => x, y => false);
resetEventFlags = Enum.GetValues<InfoMetricsType>().ToDictionary(x => x, y => false);
Comment thread
vazois marked this conversation as resolved.

public readonly Dictionary<LatencyMetricsType, bool>
resetLatencyMetrics = GarnetLatencyMetrics.defaultLatencyTypes.ToDictionary(x => x, y => false);
Expand All @@ -33,17 +33,20 @@ public readonly Dictionary<LatencyMetricsType, bool>

GarnetServerMetrics globalMetrics;
readonly GarnetSessionMetrics accSessionMetrics;
readonly CommandStats accCommandStats;
private ulong instant_input_net_bytes;
private ulong instant_output_net_bytes;
private ulong instant_commands_processed;

readonly CancellationTokenSource cts = new();
readonly ManualResetEvent done = new(false);
readonly ManualResetEvent done = new(true);

readonly ILogger logger;

public GarnetServerMetrics GlobalMetrics => globalMetrics;

internal IGarnetServer[] Servers => servers;

SingleWriterMultiReaderLock rwLock = new();

public GarnetServerMonitor(StoreWrapper storeWrapper, GarnetServerOptions opts, IGarnetServer[] servers, ILogger logger = null)
Expand All @@ -58,9 +61,10 @@ public GarnetServerMonitor(StoreWrapper storeWrapper, GarnetServerOptions opts,
instant_input_net_bytes = 0;
instant_output_net_bytes = 0;
instant_commands_processed = 0;
globalMetrics = new(true, opts.LatencyMonitor, this);
globalMetrics = new(true, opts.LatencyMonitor, opts.CommandStatsMonitor, this);

accSessionMetrics = new GarnetSessionMetrics();
accCommandStats = opts.CommandStatsMonitor ? new CommandStats() : null;
}

public void Dispose()
Expand All @@ -74,16 +78,23 @@ public void Dispose()

public void Start()
{
Task.Run(() => MainMonitorTask(cts.Token));
// Only run the periodic sampling task if a sampling frequency is configured.
// The monitor may be created solely for command stats history (without periodic sampling).
if (monitorSamplingFrequency > TimeSpan.Zero)
{
done.Reset();
Task.Run(() => MainMonitorTask(cts.Token));
}
}

public void AddMetricsHistorySessionDispose(GarnetSessionMetrics currSessionMetrics, GarnetLatencyMetricsSession currLatencyMetrics)
public void AddMetricsHistorySessionDispose(GarnetSessionMetrics currSessionMetrics, GarnetLatencyMetricsSession currLatencyMetrics, CommandStats currCommandStats = null)
{
rwLock.WriteLock();
try
{
if (currSessionMetrics != null) globalMetrics.historySessionMetrics.Add(currSessionMetrics);
if (currLatencyMetrics != null) globalMetrics.globalLatencyMetrics.Merge(currLatencyMetrics);
if (currCommandStats != null) globalMetrics.historyCommandStats?.Add(currCommandStats);
currLatencyMetrics?.Return();
}
finally { rwLock.WriteUnlock(); }
Expand Down Expand Up @@ -133,6 +144,12 @@ private void AddCurrentServerStats(IGarnetServer server)
// Accumulate session metrics
accSessionMetrics.Add(session.GetSessionMetrics);

// Accumulate command stats if enabled
if (accCommandStats != null)
{
accCommandStats.Add(session.GetCommandStats);
}

// Accumulate latency metrics if latency monitor is enabled
if (opts.LatencyMonitor)
{
Expand All @@ -154,6 +171,12 @@ private void AddCurrentServerStats(IGarnetServer server)
// Add accumulated session metrics for this iteration
globalMetrics.globalSessionMetrics.Add(accSessionMetrics);

// Reset global command stats and add accumulated for this iteration
if (accCommandStats != null)
{
globalMetrics.globalCommandStats.Reset();
globalMetrics.globalCommandStats.Add(accCommandStats);
}
}

private void CleanupGlobalStats()
Expand Down Expand Up @@ -189,6 +212,24 @@ private void CleanupGlobalStats()

resetEventFlags[InfoMetricsType.STATS] = false;
}

if (resetEventFlags.TryGetValue(InfoMetricsType.COMMANDSTATS, out bool resetCommandStats) && resetCommandStats)
{
logger?.LogInformation("Resetting command stats");
globalMetrics.globalCommandStats?.Reset();
globalMetrics.historyCommandStats?.Reset();

foreach (var garnetServer in servers.Cast<GarnetServerBase>())
{
var sessions = garnetServer.ActiveConsumers();
foreach (var s in sessions)
{
((RespServerSession)s).GetCommandStats?.Reset();
}
}

resetEventFlags[InfoMetricsType.COMMANDSTATS] = false;
}
}

private void CleanupGlobalLatencyMetrics()
Expand Down Expand Up @@ -283,6 +324,13 @@ void ResetAndAddGlobalHistory()
accSessionMetrics.Reset();
// Add session metrics history in accumulator
accSessionMetrics.Add(globalMetrics.historySessionMetrics);

// Reset command stats accumulator and add history
if (accCommandStats != null)
{
accCommandStats.Reset();
accCommandStats.Add(globalMetrics.historyCommandStats);
}
}

void ResetLatencySessionMetrics()
Expand Down
Loading
Loading