/
afanasevn
/
RedisLab
Обзор
Документация
Войти
/
afanasevn
/
RedisLab
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/RedisLab.Infrastructure/Redis/RedisDebugService.cs
155 строк
5 KB
IBS\NAfanasev
Solution commit
24 июн 2026, 16:45
24 июн 2026, 16:45
bab80b7
Код
Авторство
О чём код?
using RedisLab.Application.Abstractions; using RedisLab.Application.Contracts.Dev; using RedisLab.Application.Redis; using StackExchange.Redis; namespace RedisLab.Infrastructure.Redis; /// <summary> /// Dev introspection: только заранее известные ключи и префиксы (SCAN), не KEYS *. /// </summary> public sealed class RedisDebugService(IConnectionMultiplexer redis) : IRedisDebugService { private static readonly string[] ScanPatterns = [ RedisKeyNames.DashboardKpi, "task:*", "ratelimit:login:*", RedisKeyNames.TaskReminderLock, RedisKeyNames.LeaderboardTasksCompleted, RedisKeyNames.NotificationsStream, ]; /// <inheritdoc /> public async Task<RedisDebugInfoDto> GetInfoAsync(CancellationToken ct) { var database = redis.GetDatabase(); var endpoint = redis.GetEndPoints().FirstOrDefault()?.ToString() ?? "unknown"; var dbSize = await GetDbSizeAsync(); var keys = new List<RedisKeyEntryDto>(); foreach (var pattern in ScanPatterns) { ct.ThrowIfCancellationRequested(); await foreach (var key in ScanKeysAsync(pattern, ct)) { keys.Add(await DescribeKeyAsync(database, key)); } } keys.Add(new RedisKeyEntryDto( RedisKeyNames.ActivityFeedChannel, "pubsub", "Канал Pub/Sub (не ключ-value)", null)); return new RedisDebugInfoDto(endpoint, dbSize, keys); } /// <summary> /// SCAN по конкретному паттерну — безопаснее KEYS * в production-like коде. /// </summary> private async IAsyncEnumerable<RedisKey> ScanKeysAsync( string pattern, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken ct) { var server = redis.GetServer(redis.GetEndPoints().First()); if (!pattern.Contains('*')) { if (await redis.GetDatabase().KeyExistsAsync(pattern)) { yield return pattern; } yield break; } const int pageSize = 50; var cursor = 0L; do { ct.ThrowIfCancellationRequested(); var result = await server.ExecuteAsync("SCAN", cursor.ToString(), "MATCH", pattern, "COUNT", pageSize); var parts = (RedisResult[])result!; cursor = long.Parse(parts[0].ToString()); var batch = (RedisResult[])parts[1]!; foreach (var item in batch) { yield return (RedisKey)item.ToString(); } } while (cursor != 0); } /// <summary> /// Читает TYPE, TTL и краткое содержимое ключа для dev UI. /// </summary> private static async Task<RedisKeyEntryDto> DescribeKeyAsync( IDatabase database, RedisKey key) { var keyName = key.ToString(); var type = (await database.KeyTypeAsync(key)).ToString().ToLowerInvariant(); var ttl = await database.KeyTimeToLiveAsync(key); var ttlSeconds = ttl.HasValue ? (long)ttl.Value.TotalSeconds : (long?)null; var summary = await BuildSummaryAsync(database, key, type); return new RedisKeyEntryDto(keyName, type, summary, ttlSeconds); } /// <summary> /// Формирует короткое описание значения без выгрузки больших payload. /// </summary> private static async Task<string> BuildSummaryAsync( IDatabase database, RedisKey key, string type) { return type switch { "string" => Truncate(await database.StringGetAsync(key)), "hash" => $"fields={await database.HashLengthAsync(key)}", "zset" => $"members={await database.SortedSetLengthAsync(key)}", "stream" => await BuildStreamSummaryAsync(database, key), _ => "—", }; } /// <summary> /// Длина stream и число consumer groups. /// </summary> private static async Task<string> BuildStreamSummaryAsync( IDatabase database, RedisKey key) { var length = await database.StreamLengthAsync(key); var groups = await database.StreamGroupInfoAsync(key); return $"length={length}, groups={groups.Length}"; } /// <summary> /// DBSIZE — число ключей в текущей DB (не KEYS *). /// </summary> private async Task<long> GetDbSizeAsync() { var server = redis.GetServer(redis.GetEndPoints().First()); var result = await server.ExecuteAsync("DBSIZE"); return (long)result; } private static string Truncate(RedisValue value) { if (value.IsNullOrEmpty) { return "empty"; } var text = value.ToString(); return text.Length <= 80 ? text : text[..80] + "…"; } }