diff --git a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala index 02c824ce0..69190fb78 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/APIMethods600.scala @@ -667,8 +667,8 @@ trait APIMethods600 { "Get Cache Configuration", """Returns cache configuration information including: | - |- Available cache providers (Redis, In-Memory) - |- Redis connection details (URL, port, SSL) + |- Redis status: availability, connection details (URL, port, SSL) + |- In-memory cache status: availability and current size |- Instance ID and environment |- Global cache namespace prefix | @@ -678,21 +678,15 @@ trait APIMethods600 { |""", EmptyBody, CacheConfigJsonV600( - providers = List( - CacheProviderConfigJsonV600( - provider = "redis", - enabled = true, - url = Some("127.0.0.1"), - port = Some(6379), - use_ssl = Some(false) - ), - CacheProviderConfigJsonV600( - provider = "in_memory", - enabled = true, - url = None, - port = None, - use_ssl = None - ) + redis_status = RedisCacheStatusJsonV600( + available = true, + url = "127.0.0.1", + port = 6379, + use_ssl = false + ), + in_memory_status = InMemoryCacheStatusJsonV600( + available = true, + current_size = 42 ), instance_id = "obp", environment = "dev", @@ -738,6 +732,9 @@ trait APIMethods600 { | - "memory": Keys stored in in-memory cache | - "both": Keys in both locations (indicates a BUG - should never happen) | - "unknown": No keys found, storage location cannot be determined + |- TTL info: Sampled TTL information from actual keys + | - Shows actual TTL values from up to 5 sample keys + | - Format: "123s" (fixed), "range 60s to 3600s (avg 1800s)" (variable), "no expiry" (persistent) |- Total key count across all namespaces |- Redis availability status | @@ -755,7 +752,8 @@ trait APIMethods600 { key_count = 42, description = "Rate limit call counters", category = "Rate Limiting", - storage_location = "redis" + storage_location = "redis", + ttl_info = "range 60s to 86400s (avg 3600s)" ), CacheNamespaceInfoJsonV600( namespace_id = "rd_localised", @@ -764,7 +762,8 @@ trait APIMethods600 { key_count = 128, description = "Localized resource docs", category = "API Documentation", - storage_location = "redis" + storage_location = "redis", + ttl_info = "3600s" ) ), total_keys = 170, diff --git a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala index 2a29d7a96..36ab2d96b 100644 --- a/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala +++ b/obp-api/src/main/scala/code/api/v6_0_0/JSONFactory6.0.0.scala @@ -268,16 +268,21 @@ case class InvalidatedCacheNamespaceJsonV600( status: String ) -case class CacheProviderConfigJsonV600( - provider: String, - enabled: Boolean, - url: Option[String], - port: Option[Int], - use_ssl: Option[Boolean] +case class RedisCacheStatusJsonV600( + available: Boolean, + url: String, + port: Int, + use_ssl: Boolean +) + +case class InMemoryCacheStatusJsonV600( + available: Boolean, + current_size: Long ) case class CacheConfigJsonV600( - providers: List[CacheProviderConfigJsonV600], + redis_status: RedisCacheStatusJsonV600, + in_memory_status: InMemoryCacheStatusJsonV600, instance_id: String, environment: String, global_prefix: String @@ -290,7 +295,8 @@ case class CacheNamespaceInfoJsonV600( key_count: Int, description: String, category: String, - storage_location: String + storage_location: String, + ttl_info: String ) case class CacheInfoJsonV600( @@ -1120,21 +1126,17 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { import code.api.Constant import net.liftweb.util.Props - val redisProvider = CacheProviderConfigJsonV600( - provider = "redis", - enabled = true, - url = Some(Redis.url), - port = Some(Redis.port), - use_ssl = Some(Redis.useSsl) - ) + val redisIsReady = try { + Redis.isRedisReady + } catch { + case _: Throwable => false + } - val inMemoryProvider = CacheProviderConfigJsonV600( - provider = "in_memory", - enabled = true, - url = None, - port = None, - use_ssl = None - ) + val inMemorySize = try { + InMemory.underlyingGuavaCache.size() + } catch { + case _: Throwable => 0L + } val instanceId = code.api.util.APIUtil.getPropsValue("api_instance_id").getOrElse("obp") val environment = Props.mode match { @@ -1145,8 +1147,21 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { case _ => "unknown" } + val redisStatus = RedisCacheStatusJsonV600( + available = redisIsReady, + url = Redis.url, + port = Redis.port, + use_ssl = Redis.useSsl + ) + + val inMemoryStatus = InMemoryCacheStatusJsonV600( + available = inMemorySize >= 0, + current_size = inMemorySize + ) + CacheConfigJsonV600( - providers = List(redisProvider, inMemoryProvider), + redis_status = redisStatus, + in_memory_status = inMemoryStatus, instance_id = instanceId, environment = environment, global_prefix = Constant.getGlobalCacheNamespacePrefix @@ -1156,6 +1171,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { def createCacheInfoJsonV600(): CacheInfoJsonV600 = { import code.api.cache.{Redis, InMemory} import code.api.Constant + import code.api.JedisMethod val namespaceDescriptions = Map( Constant.CALL_COUNTER_NAMESPACE -> ("Rate limit call counters", "Rate Limiting"), @@ -1183,10 +1199,33 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { var redisKeyCount = 0 var memoryKeyCount = 0 var storageLocation = "unknown" + var ttlInfo = "no keys to sample" try { redisKeyCount = Redis.countKeys(pattern) totalKeys += redisKeyCount + + // Sample keys to get TTL information + if (redisKeyCount > 0) { + val sampleKeys = Redis.scanKeys(pattern).take(5) + val ttls = sampleKeys.flatMap { key => + Redis.use(JedisMethod.TTL, key, None, None).map(_.toLong) + } + + if (ttls.nonEmpty) { + val minTtl = ttls.min + val maxTtl = ttls.max + val avgTtl = ttls.sum / ttls.length.toLong + + ttlInfo = if (minTtl == maxTtl) { + if (minTtl == -1) "no expiry" + else if (minTtl == -2) "keys expired or missing" + else s"${minTtl}s" + } else { + s"range ${minTtl}s to ${maxTtl}s (avg ${avgTtl}s)" + } + } + } } catch { case _: Throwable => redisAvailable = false @@ -1195,6 +1234,10 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { try { memoryKeyCount = InMemory.countKeys(pattern) totalKeys += memoryKeyCount + + if (memoryKeyCount > 0 && redisKeyCount == 0) { + ttlInfo = "in-memory (no TTL in Guava cache)" + } } catch { case _: Throwable => // In-memory cache error (shouldn't happen, but handle gracefully) @@ -1203,6 +1246,7 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { // Determine storage based on where keys actually exist val keyCount = if (redisKeyCount > 0 && memoryKeyCount > 0) { storageLocation = "both" + ttlInfo = s"redis: ${ttlInfo}, memory: in-memory cache" redisKeyCount + memoryKeyCount } else if (redisKeyCount > 0) { storageLocation = "redis" @@ -1225,7 +1269,8 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { key_count = keyCount, description = description, category = category, - storage_location = storageLocation + storage_location = storageLocation, + ttl_info = ttlInfo ) } diff --git a/obp-api/src/test/scala/code/api/v6_0_0/CacheEndpointsTest.scala b/obp-api/src/test/scala/code/api/v6_0_0/CacheEndpointsTest.scala index ee8460f73..0b181a03f 100644 --- a/obp-api/src/test/scala/code/api/v6_0_0/CacheEndpointsTest.scala +++ b/obp-api/src/test/scala/code/api/v6_0_0/CacheEndpointsTest.scala @@ -158,6 +158,8 @@ class CacheEndpointsTest extends V600ServerSetup { namespace.category should not be empty namespace.storage_location should not be empty namespace.storage_location should (equal("redis") or equal("memory") or equal("both") or equal("unknown")) + namespace.ttl_info should not be empty + namespace.ttl_info shouldBe a[String] } } }