Repository: elastic/elasticsearch
Pull request #124896: Use child loggers broadly in PolicyManager
Author: login: prdoyle
Labels: name: >non-issue
name: Team:Core/Infra
description: Meta label for core/infra team
name: auto-backport
description: Automatically create backport pull requests when merged
name: v8.18.1
name: v8.19.0
name: v9.0.1
name: v9.1.0
name: :Core/Infra/Entitlements
description: Entitlements infrastructure
Created: 2025-03-14T16:11:54Z | Merged: 2025-03-15T01:30:06Z

We'd like to be able to adjust logging independently for different components / modules.


Diff:
diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
index e331b33833fae..6502b9a935034 100644
--- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
+++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
@@ -59,7 +59,10 @@
 import static java.util.zip.ZipFile.OPEN_READ;
 
 public class PolicyManager {
-    private static final Logger logger = LogManager.getLogger(PolicyManager.class);
+    /**
+     * Use this if you don't have a {@link ModuleEntitlements} in hand.
+     */
+    private static final Logger generalLogger = LogManager.getLogger(PolicyManager.class);
 
     static final String UNKNOWN_COMPONENT_NAME = "(unknown)";
     static final String SERVER_COMPONENT_NAME = "(server)";
@@ -76,7 +79,8 @@ public class PolicyManager {
     record ModuleEntitlements(
         String componentName,
         Map<Class<? extends Entitlement>, List<Entitlement>> entitlementsByType,
-        FileAccessTree fileAccess
+        FileAccessTree fileAccess,
+        Logger logger
     ) {
 
         ModuleEntitlements {
@@ -101,8 +105,13 @@ private FileAccessTree getDefaultFileAccess(String componentName, Path component
     }
 
     // pkg private for testing
-    ModuleEntitlements defaultEntitlements(String componentName, Path componentPath) {
-        return new ModuleEntitlements(componentName, Map.of(), getDefaultFileAccess(componentName, componentPath));
+    ModuleEntitlements defaultEntitlements(String componentName, Path componentPath, String moduleName) {
+        return new ModuleEntitlements(
+            componentName,
+            Map.of(),
+            getDefaultFileAccess(componentName, componentPath),
+            getLogger(componentName, moduleName)
+        );
     }
 
     // pkg private for testing
@@ -116,7 +125,8 @@ ModuleEntitlements policyEntitlements(String componentName, Path componentPath,
         return new ModuleEntitlements(
             componentName,
             entitlements.stream().collect(groupingBy(Entitlement::getClass)),
-            FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPath, exclusivePaths)
+            FileAccessTree.of(componentName, moduleName, filesEntitlement, pathLookup, componentPath, exclusivePaths),
+            getLogger(componentName, moduleName)
         );
     }
 
@@ -255,17 +265,17 @@ private void neverEntitled(Class<?> callerClass, Supplier<String> operationDescr
             return;
         }
 
-        String componentName = getEntitlements(requestingClass).componentName();
+        ModuleEntitlements entitlements = getEntitlements(requestingClass);
         notEntitled(
             Strings.format(
                 "component [%s], module [%s], class [%s], operation [%s]",
-                componentName,
+                entitlements.componentName(),
                 requestingClass.getModule().getName(),
                 requestingClass,
                 operationDescription.get()
             ),
             callerClass,
-            componentName
+            entitlements
         );
     }
 
@@ -323,7 +333,7 @@ public void checkFileRead(Class<?> callerClass, File file) {
     private static boolean isPathOnDefaultFilesystem(Path path) {
         var pathFileSystemClass = path.getFileSystem().getClass();
         if (path.getFileSystem().getClass() != DEFAULT_FILESYSTEM_CLASS) {
-            logger.trace(
+            generalLogger.trace(
                 () -> Strings.format(
                     "File entitlement trivially allowed: path [%s] is for a different FileSystem class [%s], default is [%s]",
                     path.toString(),
@@ -383,7 +393,7 @@ public void checkFileRead(Class<?> callerClass, Path path, boolean followLinks)
                     realPath == null ? path : Strings.format("%s -> %s", path, realPath)
                 ),
                 callerClass,
-                entitlements.componentName()
+                entitlements
             );
         }
     }
@@ -413,7 +423,7 @@ public void checkFileWrite(Class<?> callerClass, Path path) {
                     path
                 ),
                 callerClass,
-                entitlements.componentName()
+                entitlements
             );
         }
     }
@@ -502,18 +512,19 @@ private void checkFlagEntitlement(
                     PolicyParser.getEntitlementTypeName(entitlementClass)
                 ),
                 callerClass,
-                classEntitlements.componentName()
+                classEntitlements
             );
         }
-        logger.debug(
-            () -> Strings.format(
-                "Entitled: component [%s], module [%s], class [%s], entitlement [%s]",
-                classEntitlements.componentName(),
-                requestingClass.getModule().getName(),
-                requestingClass,
-                PolicyParser.getEntitlementTypeName(entitlementClass)
-            )
-        );
+        classEntitlements.logger()
+            .debug(
+                () -> Strings.format(
+                    "Entitled: component [%s], module [%s], class [%s], entitlement [%s]",
+                    classEntitlements.componentName(),
+                    requestingClass.getModule().getName(),
+                    requestingClass,
+                    PolicyParser.getEntitlementTypeName(entitlementClass)
+                )
+            );
     }
 
     public void checkWriteProperty(Class<?> callerClass, String property) {
@@ -524,15 +535,16 @@ public void checkWriteProperty(Class<?> callerClass, String property) {
 
         ModuleEntitlements entitlements = getEntitlements(requestingClass);
         if (entitlements.getEntitlements(WriteSystemPropertiesEntitlement.class).anyMatch(e -> e.properties().contains(property))) {
-            logger.debug(
-                () -> Strings.format(
-                    "Entitled: component [%s], module [%s], class [%s], entitlement [write_system_properties], property [%s]",
-                    entitlements.componentName(),
-                    requestingClass.getModule().getName(),
-                    requestingClass,
-                    property
-                )
-            );
+            entitlements.logger()
+                .debug(
+                    () -> Strings.format(
+                        "Entitled: component [%s], module [%s], class [%s], entitlement [write_system_properties], property [%s]",
+                        entitlements.componentName(),
+                        requestingClass.getModule().getName(),
+                        requestingClass,
+                        property
+                    )
+                );
             return;
         }
         notEntitled(
@@ -544,22 +556,34 @@ public void checkWriteProperty(Class<?> callerClass, String property) {
                 property
             ),
             callerClass,
-            entitlements.componentName()
+            entitlements
         );
     }
 
-    private void notEntitled(String message, Class<?> callerClass, String componentName) {
+    private void notEntitled(String message, Class<?> callerClass, ModuleEntitlements entitlements) {
         var exception = new NotEntitledException(message);
         // Don't emit a log for muted classes, e.g. classes containing self tests
         if (mutedClasses.contains(callerClass) == false) {
-            var moduleName = callerClass.getModule().getName();
-            var loggerSuffix = "." + componentName + "." + ((moduleName == null) ? ALL_UNNAMED : moduleName);
-            var notEntitledLogger = LogManager.getLogger(PolicyManager.class.getName() + loggerSuffix);
-            notEntitledLogger.warn("Not entitled:", exception);
+            entitlements.logger().warn("Not entitled:", exception);
         }
         throw exception;
     }
 
+    private static Logger getLogger(String componentName, String moduleName) {
+        var loggerSuffix = "." + componentName + "." + ((moduleName == null) ? ALL_UNNAMED : moduleName);
+        return MODULE_LOGGERS.computeIfAbsent(PolicyManager.class.getName() + loggerSuffix, LogManager::getLogger);
+    }
+
+    /**
+     * We want to use the same {@link Logger} object for a given name, because we want {@link ModuleEntitlements}
+     * {@code equals} and {@code hashCode} to work.
+     * <p>
+     * This would not be required if LogManager
+     * <a href="https://github.com/elastic/elasticsearch/issues/87511">memoized the loggers</a>,
+     * but here we are.
+     */
+    private static final ConcurrentHashMap<String, Logger> MODULE_LOGGERS = new ConcurrentHashMap<>();
+
     public void checkManageThreadsEntitlement(Class<?> callerClass) {
         checkEntitlementPresent(callerClass, ManageThreadsEntitlement.class);
     }
@@ -592,7 +616,7 @@ private ModuleEntitlements computeEntitlements(Class<?> requestingClass) {
         if (pluginName != null) {
             var pluginEntitlements = pluginsEntitlements.get(pluginName);
             if (pluginEntitlements == null) {
-                return defaultEntitlements(pluginName, sourcePaths.get(pluginName));
+                return defaultEntitlements(pluginName, sourcePaths.get(pluginName), requestingModule.getName());
             } else {
                 return getModuleScopeEntitlements(
                     pluginEntitlements,
@@ -613,7 +637,7 @@ private ModuleEntitlements computeEntitlements(Class<?> requestingClass) {
             );
         }
 
-        return defaultEntitlements(UNKNOWN_COMPONENT_NAME, null);
+        return defaultEntitlements(UNKNOWN_COMPONENT_NAME, null, requestingModule.getName());
     }
 
     private static String getScopeName(Module requestingModule) {
@@ -634,7 +658,7 @@ static Path getComponentPathFromClass(Class<?> requestingClass) {
             return Paths.get(codeSource.getLocation().toURI());
         } catch (Exception e) {
             // If we get a URISyntaxException, or any other Exception due to an invalid URI, we return null to safely skip this location
-            logger.info(
+            generalLogger.info(
                 "Cannot get component path for [{}]: [{}] cannot be converted to a valid Path",
                 requestingClass.getName(),
                 codeSource.getLocation().toString()
@@ -651,7 +675,7 @@ private ModuleEntitlements getModuleScopeEntitlements(
     ) {
         var entitlements = scopeEntitlements.get(scopeName);
         if (entitlements == null) {
-            return defaultEntitlements(componentName, componentPath);
+            return defaultEntitlements(componentName, componentPath, scopeName);
         }
         return policyEntitlements(componentName, componentPath, scopeName, entitlements);
     }
@@ -694,18 +718,18 @@ Optional<StackFrame> findRequestingFrame(Stream<StackFrame> frames) {
      * @return true if permission is granted regardless of the entitlement
      */
     private static boolean isTriviallyAllowed(Class<?> requestingClass) {
-        if (logger.isTraceEnabled()) {
-            logger.trace("Stack trace for upcoming trivially-allowed check", new Exception());
+        if (generalLogger.isTraceEnabled()) {
+            generalLogger.trace("Stack trace for upcoming trivially-allowed check", new Exception());
         }
         if (requestingClass == null) {
-            logger.debug("Entitlement trivially allowed: no caller frames outside the entitlement library");
+            generalLogger.debug("Entitlement trivially allowed: no caller frames outside the entitlement library");
             return true;
         }
         if (systemModules.contains(requestingClass.getModule())) {
-            logger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName());
+            generalLogger.debug("Entitlement trivially allowed from system module [{}]", requestingClass.getModule().getName());
             return true;
         }
-        logger.trace("Entitlement not trivially allowed");
+        generalLogger.trace("Entitlement not trivially allowed");
         return false;
     }
 
diff --git a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java
index e9b9d9df35f97..1a33456192726 100644
--- a/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java
+++ b/libs/entitlement/src/test/java/org/elasticsearch/entitlement/runtime/policy/PolicyManagerTests.java
@@ -102,12 +102,12 @@ public void testGetEntitlementsThrowsOnMissingPluginUnnamedModule() {
 
         assertEquals(
             "No policy for the unnamed module",
-            policyManager.defaultEntitlements("plugin1", plugin1SourcePath),
+            policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName()),
             policyManager.getEntitlements(callerClass)
         );
 
         assertEquals(
-            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath)),
+            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName())),
             policyManager.moduleEntitlementsMap
         );
     }
@@ -132,12 +132,12 @@ public void testGetEntitlementsThrowsOnMissingPolicyForPlugin() {
 
         assertEquals(
             "No policy for this plugin",
-            policyManager.defaultEntitlements("plugin1", plugin1SourcePath),
+            policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName()),
             policyManager.getEntitlements(callerClass)
         );
 
         assertEquals(
-            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath)),
+            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName())),
             policyManager.moduleEntitlementsMap
         );
     }
@@ -160,18 +160,24 @@ public void testGetEntitlementsFailureIsCached() {
         var callerClass = this.getClass();
         var requestingModule = callerClass.getModule();
 
-        assertEquals(policyManager.defaultEntitlements("plugin1", plugin1SourcePath), policyManager.getEntitlements(callerClass));
         assertEquals(
-            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath)),
+            policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName()),
+            policyManager.getEntitlements(callerClass)
+        );
+        assertEquals(
+            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName())),
             policyManager.moduleEntitlementsMap
         );
 
         // A second time
-        assertEquals(policyManager.defaultEntitlements("plugin1", plugin1SourcePath), policyManager.getEntitlements(callerClass));
+        assertEquals(
+            policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName()),
+            policyManager.getEntitlements(callerClass)
+        );
 
         // Nothing new in the map
         assertEquals(
-            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath)),
+            Map.of(requestingModule, policyManager.defaultEntitlements("plugin1", plugin1SourcePath, requestingModule.getName())),
             policyManager.moduleEntitlementsMap
         );
     }
@@ -219,12 +225,15 @@ public void testGetEntitlementsThrowsOnMissingPolicyForServer() throws ClassNotF
 
         assertEquals(
             "No policy for this module in server",
-            policyManager.defaultEntitlements(SERVER_COMPONENT_NAME, mockServerSourcePath),
+            policyManager.defaultEntitlements(SERVER_COMPONENT_NAME, mockServerSourcePath, requestingModule.getName()),
             policyManager.getEntitlements(mockServerClass)
         );
 
         assertEquals(
-            Map.of(requestingModule, policyManager.defaultEntitlements(SERVER_COMPONENT_NAME, mockServerSourcePath)),
+            Map.of(
+                requestingModule,
+                policyManager.defaultEntitlements(SERVER_COMPONENT_NAME, mockServerSourcePath, requestingModule.getName())
+            ),
             policyManager.moduleEntitlementsMap
         );
     }


Reviews:
user: login: rjernst
body: Looks fine. One nit that could be a followup as it's not performance critical (only happens in case entitlement is being denied) and will likely require some refactoring.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T16:29:29Z
user: login: prdoyle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T16:55:41Z
user: login: prdoyle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T17:20:42Z

Review comments:
diff_hunk: @@ -553,8 +565,7 @@ private void notEntitled(String message, Class<?> callerClass, String componentN
         // Don't emit a log for muted classes, e.g. classes containing self tests
         if (mutedClasses.contains(callerClass) == false) {
             var moduleName = callerClass.getModule().getName();
-            var loggerSuffix = "." + componentName + "." + ((moduleName == null) ? ALL_UNNAMED : moduleName);
-            var notEntitledLogger = LogManager.getLogger(PolicyManager.class.getName() + loggerSuffix);
+            var notEntitledLogger = getLogger(componentName, moduleName);
path: libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
user: login: rjernst
body: Shouldn't we already have the logger? The caller to  notEntitled should have grabbed ModuleEntitlements?
created_at: 2025-03-14T16:28:32Z
author_association: MEMBER
diff_hunk: @@ -553,8 +565,7 @@ private void notEntitled(String message, Class<?> callerClass, String componentN
         // Don't emit a log for muted classes, e.g. classes containing self tests
         if (mutedClasses.contains(callerClass) == false) {
             var moduleName = callerClass.getModule().getName();
-            var loggerSuffix = "." + componentName + "." + ((moduleName == null) ? ALL_UNNAMED : moduleName);
-            var notEntitledLogger = LogManager.getLogger(PolicyManager.class.getName() + loggerSuffix);
+            var notEntitledLogger = getLogger(componentName, moduleName);
path: libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
user: login: prdoyle
body: That seems cleaner. Let me try it out.
created_at: 2025-03-14T16:55:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -553,8 +565,7 @@ private void notEntitled(String message, Class<?> callerClass, String componentN
         // Don't emit a log for muted classes, e.g. classes containing self tests
         if (mutedClasses.contains(callerClass) == false) {
             var moduleName = callerClass.getModule().getName();
-            var loggerSuffix = "." + componentName + "." + ((moduleName == null) ? ALL_UNNAMED : moduleName);
-            var notEntitledLogger = LogManager.getLogger(PolicyManager.class.getName() + loggerSuffix);
+            var notEntitledLogger = getLogger(componentName, moduleName);
path: libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java
user: login: prdoyle
body: Yeah that's much better.
created_at: 2025-03-14T17:20:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticsearchmachine
created_at: 2025-03-14T16:12:18Z
body: Pinging @elastic/es-core-infra (Team:Core/Infra)
author_association: COLLABORATOR
user: login: prdoyle
created_at: 2025-03-14T16:25:16Z
body: D'oh indeed, Homer, d'oh indeed.

```
expected:<ModuleEntitlements[componentName=plugin1, entitlementsByType={}, fileAccess=org.elasticsearch.entitlement.runtime.policy.FileAccessTree@7b5089eb, logger=org.elasticsearch.common.logging.internal.LoggerImpl@33cd69a]>
but was:<ModuleEntitlements[componentName=plugin1, entitlementsByType={}, fileAccess=org.elasticsearch.entitlement.runtime.policy.FileAccessTree@7b5089eb, logger=org.elasticsearch.common.logging.internal.LoggerImpl@199d0076]>
```
author_association: CONTRIBUTOR
user: login: prdoyle
created_at: 2025-03-14T17:31:10Z
body: FYI this will conflict with #124895.

**Update**: I've resolved the conflict.
author_association: CONTRIBUTOR
user: login: elasticsearchmachine
created_at: 2025-03-15T01:31:54Z
body: ## 💚 Backport successful
| Status | Branch | Result |
|:------:|:------:|:------:|
| ✅ |  [8.18](https://github.com/elastic/elasticsearch/pull/124937)  | [<img src="https://img.shields.io/github/pulls/detail/state/elastic/elasticsearch/124937">](https://github.com/elastic/elasticsearch/pull/124937) |
| ✅ |  [8.x](https://github.com/elastic/elasticsearch/pull/124938)  | [<img src="https://img.shields.io/github/pulls/detail/state/elastic/elasticsearch/124938">](https://github.com/elastic/elasticsearch/pull/124938) |
| ✅ |  [9.0](https://github.com/elastic/elasticsearch/pull/124939)  | [<img src="https://img.shields.io/github/pulls/detail/state/elastic/elasticsearch/124939">](https://github.com/elastic/elasticsearch/pull/124939) |
author_association: COLLABORATOR