From 4a20168da7cf2056a2833364aeee522130d121c1 Mon Sep 17 00:00:00 2001 From: simonredfern Date: Wed, 31 Dec 2025 17:18:08 +0100 Subject: [PATCH] Added GET system cache config and GET system cache info --- .../main/scala/code/api/util/ApiRole.scala | 6 + .../scala/code/api/v6_0_0/APIMethods600.scala | 129 +++++++++++++++++- .../code/api/v6_0_0/JSONFactory6.0.0.scala | 123 +++++++++++++++++ 3 files changed, 257 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index a8ec0c221..c025fe7c2 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -412,6 +412,12 @@ object ApiRole extends MdcLoggable{ lazy val canGetMetricsAtOneBank = CanGetMetricsAtOneBank() case class CanGetConfig(requiresBankId: Boolean = false) extends ApiRole + case class CanGetCacheConfig(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetCacheConfig = CanGetCacheConfig() + + case class CanGetCacheInfo(requiresBankId: Boolean = false) extends ApiRole + lazy val canGetCacheInfo = CanGetCacheInfo() + case class CanGetCacheNamespaces(requiresBankId: Boolean = false) extends ApiRole lazy val canGetCacheNamespaces = CanGetCacheNamespaces() 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 10457edd5..70a3f3565 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 @@ -27,7 +27,7 @@ import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500} import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510} import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo} import code.api.v6_0_0.JSONFactory600.{AddUserToGroupResponseJsonV600, DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupEntitlementJsonV600, GroupEntitlementsJsonV600, GroupJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, UserGroupMembershipJsonV600, UserGroupMembershipsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveRateLimitsJsonV600, createCallLimitJsonV600, createRedisCallCountersJson} -import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CreateAbacRuleJsonV600, CurrentConsumerJsonV600, ExecuteAbacRuleJsonV600, UpdateAbacRuleJsonV600} +import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CacheConfigJsonV600, CacheInfoJsonV600, CacheNamespaceInfoJsonV600, CacheProviderConfigJsonV600, CreateAbacRuleJsonV600, CurrentConsumerJsonV600, ExecuteAbacRuleJsonV600, UpdateAbacRuleJsonV600} import code.api.v6_0_0.OBPAPI6_0_0 import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider} import code.metrics.APIMetrics @@ -658,6 +658,133 @@ trait APIMethods600 { } } + staticResourceDocs += ResourceDoc( + getCacheConfig, + implementedInApiVersion, + nameOf(getCacheConfig), + "GET", + "/system/cache/config", + "Get Cache Configuration", + """Returns cache configuration information including: + | + |- Available cache providers (Redis, In-Memory) + |- Redis connection details (URL, port, SSL) + |- Instance ID and environment + |- Global cache namespace prefix + | + |This helps understand what cache backend is being used and how it's configured. + | + |Authentication is Required + |""", + 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 + ) + ), + instance_id = "obp", + environment = "dev", + global_prefix = "obp_dev_" + ), + List( + UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagCache, apiTagSystem, apiTagApi), + Some(List(canGetCacheConfig)) + ) + + lazy val getCacheConfig: OBPEndpoint = { + case "system" :: "cache" :: "config" :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canGetCacheConfig, callContext) + } yield { + val result = JSONFactory600.createCacheConfigJsonV600() + (result, HttpCode.`200`(callContext)) + } + } + } + + staticResourceDocs += ResourceDoc( + getCacheInfo, + implementedInApiVersion, + nameOf(getCacheInfo), + "GET", + "/system/cache/info", + "Get Cache Information", + """Returns detailed cache information for all namespaces: + | + |- Namespace ID and versioned prefix + |- Current version counter + |- Number of keys in each namespace + |- Description and category + |- Total key count across all namespaces + |- Redis availability status + | + |This endpoint helps monitor cache usage and identify which namespaces contain the most data. + | + |Authentication is Required + |""", + EmptyBody, + CacheInfoJsonV600( + namespaces = List( + CacheNamespaceInfoJsonV600( + namespace_id = "call_counter", + prefix = "obp_dev_call_counter_1_", + current_version = 1, + key_count = 42, + description = "Rate limit call counters", + category = "Rate Limiting" + ), + CacheNamespaceInfoJsonV600( + namespace_id = "rd_localised", + prefix = "obp_dev_rd_localised_1_", + current_version = 1, + key_count = 128, + description = "Localized resource docs", + category = "API Documentation" + ) + ), + total_keys = 170, + redis_available = true + ), + List( + UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagCache, apiTagSystem, apiTagApi), + Some(List(canGetCacheInfo)) + ) + + lazy val getCacheInfo: OBPEndpoint = { + case "system" :: "cache" :: "info" :: Nil JsonGet _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (Full(u), callContext) <- authenticatedAccess(cc) + _ <- NewStyle.function.hasEntitlement("", u.userId, canGetCacheInfo, callContext) + } yield { + val result = JSONFactory600.createCacheInfoJsonV600() + (result, HttpCode.`200`(callContext)) + } + } + } + lazy val getCurrentConsumer: OBPEndpoint = { case "consumers" :: "current" :: Nil JsonGet _ => { cc => { 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 8ea1b07de..ae8587f8b 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,6 +268,36 @@ case class InvalidatedCacheNamespaceJsonV600( status: String ) +case class CacheProviderConfigJsonV600( + provider: String, + enabled: Boolean, + url: Option[String], + port: Option[Int], + use_ssl: Option[Boolean] +) + +case class CacheConfigJsonV600( + providers: List[CacheProviderConfigJsonV600], + instance_id: String, + environment: String, + global_prefix: String +) + +case class CacheNamespaceInfoJsonV600( + namespace_id: String, + prefix: String, + current_version: Long, + key_count: Int, + description: String, + category: String +) + +case class CacheInfoJsonV600( + namespaces: List[CacheNamespaceInfoJsonV600], + total_keys: Int, + redis_available: Boolean +) + case class PostCustomerJsonV600( legal_name: String, customer_number: Option[String] = None, @@ -1083,4 +1113,97 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable { ): CacheNamespacesJsonV600 = { CacheNamespacesJsonV600(namespaces) } + + def createCacheConfigJsonV600(): CacheConfigJsonV600 = { + import code.api.cache.{Redis, InMemory} + 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 inMemoryProvider = CacheProviderConfigJsonV600( + provider = "in_memory", + enabled = true, + url = None, + port = None, + use_ssl = None + ) + + val instanceId = code.api.util.APIUtil.getPropsValue("api_instance_id").getOrElse("obp") + val environment = Props.mode match { + case Props.RunModes.Production => "prod" + case Props.RunModes.Staging => "staging" + case Props.RunModes.Development => "dev" + case Props.RunModes.Test => "test" + case _ => "unknown" + } + + CacheConfigJsonV600( + providers = List(redisProvider, inMemoryProvider), + instance_id = instanceId, + environment = environment, + global_prefix = Constant.getGlobalCacheNamespacePrefix + ) + } + + def createCacheInfoJsonV600(): CacheInfoJsonV600 = { + import code.api.cache.Redis + import code.api.Constant + + val namespaceDescriptions = Map( + Constant.CALL_COUNTER_NAMESPACE -> ("Rate limit call counters", "Rate Limiting"), + Constant.RL_ACTIVE_NAMESPACE -> ("Active rate limit states", "Rate Limiting"), + Constant.RD_LOCALISED_NAMESPACE -> ("Localized resource docs", "API Documentation"), + Constant.RD_DYNAMIC_NAMESPACE -> ("Dynamic resource docs", "API Documentation"), + Constant.RD_STATIC_NAMESPACE -> ("Static resource docs", "API Documentation"), + Constant.RD_ALL_NAMESPACE -> ("All resource docs", "API Documentation"), + Constant.SWAGGER_STATIC_NAMESPACE -> ("Static Swagger docs", "API Documentation"), + Constant.CONNECTOR_NAMESPACE -> ("Connector cache", "Connector"), + Constant.METRICS_STABLE_NAMESPACE -> ("Stable metrics data", "Metrics"), + Constant.METRICS_RECENT_NAMESPACE -> ("Recent metrics data", "Metrics"), + Constant.ABAC_RULE_NAMESPACE -> ("ABAC rule cache", "Authorization") + ) + + var redisAvailable = true + var totalKeys = 0 + + val namespaces = Constant.ALL_CACHE_NAMESPACES.map { namespaceId => + val version = Constant.getCacheNamespaceVersion(namespaceId) + val prefix = Constant.getVersionedCachePrefix(namespaceId) + val pattern = s"${prefix}*" + + val keyCount = try { + val count = Redis.countKeys(pattern) + totalKeys += count + count + } catch { + case _: Throwable => + redisAvailable = false + 0 + } + + val (description, category) = namespaceDescriptions.getOrElse(namespaceId, ("Unknown namespace", "Other")) + + CacheNamespaceInfoJsonV600( + namespace_id = namespaceId, + prefix = prefix, + current_version = version, + key_count = keyCount, + description = description, + category = category + ) + } + + CacheInfoJsonV600( + namespaces = namespaces, + total_keys = totalKeys, + redis_available = redisAvailable + ) + } }