diff --git a/documentation/jetty/modules/operations-guide/pages/deploy/index.adoc b/documentation/jetty/modules/operations-guide/pages/deploy/index.adoc index 2805fb9e9226..2e43458416bb 100644 --- a/documentation/jetty/modules/operations-guide/pages/deploy/index.adoc +++ b/documentation/jetty/modules/operations-guide/pages/deploy/index.adoc @@ -202,18 +202,26 @@ $ java -jar $JETTY_HOME/start.jar -Dmyapps.dir=/opt/myapps [[env-context-xml]] == Deploying Environment Specific Context XML Files -A xref:xml/index.adoc[Jetty context XML file] file can be applied to _all_ webapps deployed to a particular `environment` by adding a file named `{ee-all}.properties` containing the location of the file. +xref:xml/index.adoc[Jetty context XML file] files can be applied to _all_ webapps deployed to a particular `environment`. -This environment-specific Jetty context XML file will be applied to the webapps _before_ any context XML file associated with the webapp. -Thus, it can be used to configure general defaults to all webapps for a given `environment`, allowing a webapp-specific context XML file to further refine or override that configuration. +Add a properties file to the deployment directory that contains a property naming the location of the xml file to apply. +The properties file name must be prefixed by the name of the `environment`. +For example `ee8.properties, ee8-more.properties ee8-other.properties` would all apply to all contexts deployed in the `ee8` environment. + +If the property file(s) contain one or more properties whose names are prefixed with `org.eclipse.jetty.deploy.environmentXml`, then their values are used as additional context XML files to apply to all contexts deployed in the corresponding environment. +So for example `org.eclipse.jetty.deploy.environmentXml, org.eclipse.jetty.deploy.environmentXml.more, org.eclipse.jetty.deploy.environmentXml.other` are all acceptable as names. +Each property configures the location of a context XML file to apply to a context when it is being created and deployed. +The location may be either absolute or relative to _the parent of the deployment directory_. +So if your webapp deployment directory is `$JETTY_BASE/webapps`, then `$JETTY_BASE` will be used to resolve any relative filenames. + +All environment-specific Jetty context XML files will be applied to the webapp _before_ any context XML file associated with the webapp. +The order in which they are applied is determined by _the name of the properties that define them_. IMPORTANT: The contents of the environment specific context XML file may only contain references to classes appropriate for that environment. -The `{ee-all}.properties` file must be in the same directory as the webapp being deployed. -It must contain the property `jetty.deploy.environmentXml` set to the location of the context XML file. -The location may be either absolute or relative to the parent of the deployment directory (usually `$JETTY_BASE`). +For example, given the previous example of a `$JETTY_BASE/webapps/wiki.xml` and its accompanying `$JETTY_BASE/webapps/wiki.properties` file that declares the `wiki` webapp should be deployed to environment `{ee-current}`, files called `$JETTY_BASE/webapps/{ee-current}.properties` and `$JETTY_BASE/webapps/{ee-current}-feature.properties` can be defined to further configure the webapp. -For example, given the previous example of a `$JETTY_BASE/webapps/wiki.xml` and its accompanying `$JETTY_BASE/webapps/wiki.properties` file that declares the `wiki` webapp should be deployed to environment `{ee-current}`, a file called `$JETTY_BASE/webapps/{ee-current}.properties` can be defined containing the following: +The `{ee-current}.properties` file contains: .{ee-current}.properties [,properties,subs=attributes+] @@ -221,21 +229,65 @@ For example, given the previous example of a `$JETTY_BASE/webapps/wiki.xml` and jetty.deploy.environmentXml=etc/{ee-current}-context.xml ---- +The `{ee-current}-feature.properties` file contains: + +.{ee-current}-feature.properties +[,properties,subs=attributes+] +---- +jetty.deploy.environmentXml.feature=etc/{ee-current}-feature.xml +---- + +The `{ee-current}-context.xml` file contains: + +.{ee-current}-context.xml +[,xml,subs="attributes+,+quotes"] +---- + + + + + + common + value + + +---- + +The `{ee-current}-feature.xml` file contains: + +.{ee-current}-feature.xml +[,xml,subs="attributes+,+quotes"] +---- + + + + + + + + + + +---- + + The directory structure would look like this: .directory structure [,properties,subs=attributes+] ---- $JETTY_BASE -├── etc -│ └── {ee-current}-context.xml -└── webapps - ├── {ee-current}.properties - ├── wiki.properties - └── wiki.xml +|- etc +│ |-{ee-current}-context.xml +| |-{ee-current}-feature.xml +|- webapps + |-{ee-current}.properties + |-{ee-current}-feature.properties + |-wiki.properties + |- wiki.xml ---- -The contents of the `$JETTY_BASE/etc/{ee-current}-context.xml` file will be applied to the `wiki` webapp instance _before_ the `wiki.xml`, allowing the contents of the latter to override the contents of the former. +The contents of the `$JETTY_BASE/etc/{ee-current}-context.xml` then `$JETTY_BASE/etc/{ee-current}-feature.xml` files will be applied to the `wiki` webapp instance _before_ `wiki.xml`, allowing the contents of the latter to override the contents of the former. == WEB-INF/jetty-{ee-all}-web.xml @@ -264,7 +316,7 @@ The JNDI entry must be _defined_ in a xref:jndi/index.adoc#xml[Jetty XML file], /mywebapp /opt/webapps/mywebapp.war - # + jdbc/myds @@ -274,7 +326,7 @@ The JNDI entry must be _defined_ in a xref:jndi/index.adoc#xml[Jetty XML file], password - # + ---- diff --git a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc index d3992234c719..3c96a3942664 100644 --- a/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc +++ b/documentation/jetty/modules/operations-guide/pages/modules/standard.adoc @@ -93,11 +93,26 @@ The module properties are: include::{jetty-home}/modules/cross-origin.mod[tags=documentation] ---- -You must configure at least the property `jetty.crossorigin.allowedOriginPatterns` to allow one or more origins. +[[debuglog]] +== Module `debuglog` + +The `debuglog` module logs extra information about request processing. + +This module installs the `DebugHandler` into the handler tree and logs information into a rollover log file. +By default, the location of the log file is `$JETTY_BASE/logs`, although this can be configured via the `jetty.debuglog.dir`. +The name of the log file is of the form: `yyyy_mm_dd.debug.log`, for example `2024-10-01.debug.log`. +All deployed contexts will write to the same log file. + +Note that if a request uses asynchronous processing, the log event for the completion of handling may occur out of order with the log event for the continuation of processing. + +If more precise logging of request processing is required - including well-ordered async processing events - or you wish to constrain request debug logging to particular contexts only, then configure an ee-specific `DebugListener` for each context instead of using this module. + +The module properties are: + +---- +include::{jetty-home}/modules/debuglog.mod[tags=documentation] +---- -It is recommended that you consider configuring also the property `jetty.crossorigin.allowCredentials`. -When set to `true`, clients send cookies and authentication headers in cross-origin requests to your domain. -When set to `false`, cookies and authentication headers are not sent. [[eeN-deploy]] == Module `{ee-all}-deploy` diff --git a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java index f68a8fa353af..db604b422eed 100644 --- a/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java +++ b/jetty-core/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ContextProvider.java @@ -24,12 +24,15 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jetty.deploy.App; @@ -302,7 +305,10 @@ else if (Supplier.class.isAssignableFrom(context.getClass())) initializeContextPath(contextHandler, path); if (Files.isDirectory(path)) + { contextHandler.setBaseResource(ResourceFactory.of(this).newResource(path)); + System.err.println("SET BASE RESOURCE to " + path); + } //TODO think of better way of doing this //pass through properties as attributes directly @@ -356,25 +362,79 @@ public ContextHandler createContextHandler(final App app) throws Exception if (contextHandlerClassName != null) context = Class.forName(contextHandlerClassName).getDeclaredConstructor().newInstance(); - //add in environment-specific properties + //Add in environment-specific properties: + // allow multiple eeXX[-zzz].properties files, ordered lexically + // allow each to contain jetty.deploy.environmentXml[.zzzz] properties + // accumulate all properties for substitution purposes + // order all jetty.deploy.environmentXml[.zzzz] properties lexically + // apply the context xml files named by the ordered jetty.deploy.environmentXml[.zzzz] properties String env = app.getEnvironmentName() == null ? "" : app.getEnvironmentName(); - Path envProperties = app.getPath().getParent().resolve(env + ".properties"); - if (Files.exists(envProperties)) + + if (StringUtil.isNotBlank(env)) { - try (InputStream stream = Files.newInputStream(envProperties)) + List envPropertyFiles = new ArrayList<>(); + Path parent = app.getPath().getParent(); + + //Get all environment specific properties files for this environment, + //order them according to the lexical ordering of the filenames + try (Stream paths = Files.list(parent)) + { + envPropertyFiles = paths.filter(Files::isRegularFile) + .map(p -> parent.relativize(p)) + .filter(p -> + { + String name = p.getName(0).toString(); + if (!name.endsWith(".properties")) + return false; + if (!name.startsWith(env)) + return false; + return true; + }).sorted().collect(Collectors.toList()); + } + + if (LOG.isDebugEnabled()) + LOG.debug("Environment property files {}", envPropertyFiles); + + Map envXmlFilenameMap = new HashMap<>(); + for (Path file : envPropertyFiles) { - Properties p = new Properties(); - p.load(stream); - p.stringPropertyNames().forEach(k -> properties.put(k, p.getProperty(k))); + Path resolvedFile = parent.resolve(file); + if (Files.exists(resolvedFile)) + { + Properties tmp = new Properties(); + try (InputStream stream = Files.newInputStream(resolvedFile)) + { + tmp.load(stream); + //put each property into our substitution pool + tmp.stringPropertyNames().forEach(k -> properties.put(k, tmp.getProperty(k))); + } + } } - String str = properties.get(Deployable.ENVIRONMENT_XML); - if (!StringUtil.isEmpty(str)) + //extract any properties that name environment context xml files + for (Map.Entry entry : properties.entrySet()) { - Path envXmlPath = Paths.get(str); - if (!envXmlPath.isAbsolute()) - envXmlPath = getMonitoredDirResource().getPath().getParent().resolve(envXmlPath); + String name = Objects.toString(entry.getKey(), ""); + if (name.startsWith(Deployable.ENVIRONMENT_XML)) + { + //ensure all environment context xml files are absolute paths + Path envXmlPath = Paths.get(entry.getValue().toString()); + if (!envXmlPath.isAbsolute()) + envXmlPath = getMonitoredDirResource().getPath().getParent().resolve(envXmlPath); + //accumulate all properties that name environment xml files so they can be ordered + envXmlFilenameMap.put(name, envXmlPath); + } + } + //order the environment context xml files according to the name of their properties + List sortedEnvXmlProperties = envXmlFilenameMap.keySet().stream().sorted().toList(); + + //apply each environment context xml file + for (String property : sortedEnvXmlProperties) + { + Path envXmlPath = envXmlFilenameMap.get(property); + if (LOG.isDebugEnabled()) + LOG.debug("Applying environment specific context file {}", envXmlPath); context = applyXml(context, envXmlPath, env, properties); } } @@ -427,9 +487,10 @@ else if (!Files.isDirectory(path) && !FileID.isWebArchive(path)) throw new IllegalStateException("Unknown ContextHandler class " + contextHandlerClassName + " for " + app); context = contextHandlerClass.getDeclaredConstructor().newInstance(); - properties.put(Deployable.WAR, path.toString()); } + //set a backup value for the path to the war in case it hasn't already been set + properties.put(Deployable.WAR, path.toString()); return initializeContextHandler(context, path, properties); } finally diff --git a/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java index aa8acefca239..a68db97d0e66 100644 --- a/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java +++ b/jetty-core/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/ContextProviderStartupTest.java @@ -13,6 +13,7 @@ package org.eclipse.jetty.deploy.providers; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -34,7 +35,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -88,7 +91,6 @@ public void testStartupContext() throws Exception // Check Server for Handlers jetty.assertContextHandlerExists("/bar"); - } @Test @@ -96,17 +98,24 @@ public void testStartupWithRelativeEnvironmentContext() throws Exception { Path jettyBase = jetty.getJettyBasePath(); Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = etc/core-context.xml", StandardOpenOption.CREATE_NEW); + Path props2File = Files.writeString(jettyBase.resolve("webapps/core-other.properties"), Deployable.ENVIRONMENT_XML + ".other = etc/core-context-other.xml", StandardOpenOption.CREATE_NEW); assertTrue(Files.exists(propsFile)); + assertTrue(Files.exists(props2File)); Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING); + Files.copy(MavenPaths.findTestResourceFile("etc/core-context-other.xml"), jettyBase.resolve("etc/core-context-other.xml"), StandardCopyOption.REPLACE_EXISTING); + jetty.copyWebapp("bar-core-context.properties", "bar.properties"); startJetty(); - //check environment context xml was applied to the produced context + //check core-context.xml was applied to the produced context ContextHandler context = jetty.getContextHandler("/bar"); assertNotNull(context); - assertThat(context.getAttribute("somename"), equalTo("somevalue")); + assertThat(context.getAttribute("core-context-0"), equalTo("core-context-0")); assertTrue(context instanceof BarContextHandler); - + //check core-context-other.xml was applied to the produced context + assertThat(context.getAttribute("other"), equalTo("othervalue")); + //check core-context-other.xml was applied AFTER core-context.xml + assertThat(context.getAttribute("somename"), equalTo("othervalue")); } @Test @@ -116,13 +125,108 @@ public void testStartupWithAbsoluteEnvironmentContext() throws Exception Path propsFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = " + MavenPaths.findTestResourceFile("etc/core-context.xml"), StandardOpenOption.CREATE_NEW); assertTrue(Files.exists(propsFile)); + Path props2File = Files.writeString(jettyBase.resolve("webapps/core-other.properties"), Deployable.ENVIRONMENT_XML + ".other = " + MavenPaths.findTestResourceFile("etc/core-context-other.xml"), StandardOpenOption.CREATE_NEW); + assertTrue(Files.exists(props2File)); Files.copy(MavenPaths.findTestResourceFile("etc/core-context.xml"), jettyBase.resolve("etc/core-context.xml"), StandardCopyOption.REPLACE_EXISTING); - jetty.copyWebapp("bar-core-context.properties", "bar-core-context.properties"); + Files.copy(MavenPaths.findTestResourceFile("etc/core-context-other.xml"), jettyBase.resolve("etc/core-context-other.xml"), StandardCopyOption.REPLACE_EXISTING); + jetty.copyWebapp("bar-core-context.properties", "bar.properties"); startJetty(); - //check environment context xml was applied to the produced context + //check core environment context xml was applied to the produced context ContextHandler context = jetty.getContextHandler("/bar"); assertNotNull(context); - assertThat(context.getAttribute("somename"), equalTo("somevalue")); + assertThat(context.getAttribute("core-context-0"), equalTo("core-context-0")); + assertTrue(context instanceof BarContextHandler); + //check core-context-other.xml was applied to the produced context + assertThat(context.getAttribute("other"), equalTo("othervalue")); + //check core-context-other.xml was applied AFTER core-context.xml + assertThat(context.getAttribute("somename"), equalTo("othervalue")); + } + + @Test + public void testNonEnvironmentPropertyFileNotApplied() throws Exception + { + Path jettyBase = jetty.getJettyBasePath(); + Path propsFile = Files.writeString(jettyBase.resolve("webapps/non-env.properties"), Deployable.ENVIRONMENT_XML + " = some/file/that/should/be/ignored.txt", StandardOpenOption.CREATE_NEW); + Path propsEE8File = Files.writeString(jettyBase.resolve("webapps/ee8.properties"), Deployable.ENVIRONMENT_XML + " = some/file/that/should/be/ignored.txt", StandardOpenOption.CREATE_NEW); + Path propsEE9File = Files.writeString(jettyBase.resolve("webapps/ee9.properties"), Deployable.ENVIRONMENT_XML + " = some/file/that/should/be/ignored.txt", StandardOpenOption.CREATE_NEW); + Path propsEE10File = Files.writeString(jettyBase.resolve("webapps/ee10.properties"), Deployable.ENVIRONMENT_XML + " = some/file/that/should/be/ignored.txt", StandardOpenOption.CREATE_NEW); + Path propsNonCoreFile = Files.writeString(jettyBase.resolve("webapps/not-core.properties"), Deployable.ENVIRONMENT_XML + " = some/file/that/should/be/ignored.txt", StandardOpenOption.CREATE_NEW); + assertTrue(Files.exists(propsFile)); + assertTrue(Files.exists(propsEE8File)); + assertTrue(Files.exists(propsEE9File)); + assertTrue(Files.exists(propsEE10File)); + jetty.copyWebapp("bar-core-context.properties", "bar.properties"); + startJetty(); + + //test that the context was deployed as expected and that the non-applicable properties files were ignored + ContextHandler context = jetty.getContextHandler("/bar"); + assertNotNull(context); + assertTrue(context instanceof BarContextHandler); + } + + /** + * Test that properties of the same name will be overridden, in the order of the name of the .properties file + * @throws Exception + */ + @Test + public void testPropertyOverriding() throws Exception + { + Path jettyBase = jetty.getJettyBasePath(); + Path propsCoreAFile = Files.writeString(jettyBase.resolve("webapps/core-a.properties"), Deployable.ENVIRONMENT_XML + " = etc/a.xml", StandardOpenOption.CREATE_NEW); + Path propsCoreBFile = Files.writeString(jettyBase.resolve("webapps/core-b.properties"), Deployable.ENVIRONMENT_XML + " = etc/b.xml", StandardOpenOption.CREATE_NEW); + Path propsCoreCFile = Files.writeString(jettyBase.resolve("webapps/core-c.properties"), Deployable.ENVIRONMENT_XML + " = etc/c.xml", StandardOpenOption.CREATE_NEW); + Path propsCoreDFile = Files.writeString(jettyBase.resolve("webapps/core-d.properties"), Deployable.ENVIRONMENT_XML + " = etc/d.xml", StandardOpenOption.CREATE_NEW); + assertTrue(Files.exists(propsCoreAFile)); + assertTrue(Files.exists(propsCoreBFile)); + assertTrue(Files.exists(propsCoreCFile)); + assertTrue(Files.exists(propsCoreDFile)); + Path aPath = jettyBase.resolve("etc/a.xml"); + writeXmlDisplayName(aPath, "A WebApp"); + Path bPath = jettyBase.resolve("etc/b.xml"); + writeXmlDisplayName(bPath, "B WebApp"); + Path cPath = jettyBase.resolve("etc/c.xml"); + writeXmlDisplayName(cPath, "C WebApp"); + Path dPath = jettyBase.resolve("etc/d.xml"); + writeXmlDisplayName(dPath, "D WebApp"); + assertTrue(Files.exists(propsCoreAFile)); + assertTrue(Files.exists(propsCoreBFile)); + assertTrue(Files.exists(propsCoreCFile)); + assertTrue(Files.exists(propsCoreDFile)); + + jetty.copyWebapp("bar-core-context.properties", "bar.properties"); + startJetty(); + + ContextHandler context = jetty.getContextHandler("/bar"); + assertNotNull(context); + assertEquals("D WebApp", context.getDisplayName()); + } + + /** + * Test that properties defined in an environment-specific properties file + * are used for substitution. + * + * @throws Exception + */ + @Test + public void testPropertySubstitution() throws Exception + { + Path jettyBase = jetty.getJettyBasePath(); + Path propsCoreAFile = Files.writeString(jettyBase.resolve("webapps/core.properties"), Deployable.ENVIRONMENT_XML + " = etc/core-context-sub.xml\ntest.displayName=DisplayName Set By Property", StandardOpenOption.CREATE_NEW); + Files.copy(MavenPaths.findTestResourceFile("etc/core-context-sub.xml"), jettyBase.resolve("etc/core-context-sub.xml"), StandardCopyOption.REPLACE_EXISTING); + jetty.copyWebapp("bar-core-context.properties", "bar.properties"); + startJetty(); + ContextHandler context = jetty.getContextHandler("/bar"); + assertNotNull(context); + assertEquals("DisplayName Set By Property", context.getDisplayName()); + } + + private static void writeXmlDisplayName(Path filePath, String displayName) throws IOException + { + String content = "\n" + + "\n" + + " "; + + Files.writeString(filePath, content + displayName + "\n", StandardOpenOption.CREATE_NEW); } } diff --git a/jetty-core/jetty-deploy/src/test/resources/etc/core-context-other.xml b/jetty-core/jetty-deploy/src/test/resources/etc/core-context-other.xml new file mode 100644 index 000000000000..3879d3d117bc --- /dev/null +++ b/jetty-core/jetty-deploy/src/test/resources/etc/core-context-other.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + other + othervalue + + + somename + othervalue + + diff --git a/jetty-core/jetty-deploy/src/test/resources/etc/core-context-sub.xml b/jetty-core/jetty-deploy/src/test/resources/etc/core-context-sub.xml new file mode 100644 index 000000000000..ff3c5b7caa16 --- /dev/null +++ b/jetty-core/jetty-deploy/src/test/resources/etc/core-context-sub.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml b/jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml index cccca5c2950c..ef4e4e2f236a 100644 --- a/jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml +++ b/jetty-core/jetty-deploy/src/test/resources/etc/core-context.xml @@ -19,4 +19,8 @@ somename somevalue + + core-context-0 + core-context-0 + diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-debug.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-debug.xml deleted file mode 100644 index 0e091f017e4c..000000000000 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-debug.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - /yyyy_mm_dd.debug.log - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-debuglog.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-debuglog.xml index 31500c94753f..af4ec8f6507f 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-debuglog.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-debuglog.xml @@ -9,17 +9,18 @@ - - - /yyyy_mm_dd.debug.log - - - - - - - - + + + /yyyy_mm_dd.debug.log + + + + + + + + + diff --git a/jetty-core/jetty-server/src/main/config/modules/debug.mod b/jetty-core/jetty-server/src/main/config/modules/debug.mod deleted file mode 100644 index b30cc8dde4c3..000000000000 --- a/jetty-core/jetty-server/src/main/config/modules/debug.mod +++ /dev/null @@ -1,42 +0,0 @@ -# DO NOT EDIT THIS FILE - See: https://jetty.org/docs/ - -[description] -Enables the DebugListener. -Generates additional logging regarding detailed request handling events. -Renames threads to include request URI. - -[tags] -server -debug - -[depend] -deploy - -[files] -logs/ - -[xml] -etc/jetty-debug.xml - -[ini-template] - -## How many days to retain old log files -# jetty.debug.retainDays=14 - -## Should existing log be appended to -# jetty.debug.append=true - -## Log directory for jetty debug logs -# jetty.debug.logs=./logs - -## Timezone of the log entries -# jetty.debug.timezone=GMT - -## Show Request/Response headers -# jetty.debug.showHeaders=true - -## Rename threads while in context scope -# jetty.debug.renameThread=false - -## Dump context as deployed -# jetty.debug.dumpContext=true diff --git a/jetty-core/jetty-server/src/main/config/modules/debuglog.mod b/jetty-core/jetty-server/src/main/config/modules/debuglog.mod index de8ec60b7851..4aaeaf0dfe08 100644 --- a/jetty-core/jetty-server/src/main/config/modules/debuglog.mod +++ b/jetty-core/jetty-server/src/main/config/modules/debuglog.mod @@ -1,8 +1,7 @@ # DO NOT EDIT THIS FILE - See: https://jetty.org/docs/ [description] -Deprecated Debug Log using DebugHandle. -Replaced with the debug module. +Debug Log using DebugHandle. [tags] server @@ -19,6 +18,7 @@ logs/ etc/jetty-debuglog.xml [ini-template] +#tag::documentation[] ## Logging directory (relative to $jetty.base) # jetty.debuglog.dir=logs @@ -30,3 +30,7 @@ etc/jetty-debuglog.xml ## Timezone of the log entries # jetty.debuglog.timezone=GMT + +## Show Request/Response headers +# jetty.debug.showHeaders=true +#end::documentation[] \ No newline at end of file diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java index d8454e4eb54e..43b2c6a3d9a0 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DebugHandler.java @@ -16,8 +16,8 @@ import java.io.OutputStream; import java.io.PrintStream; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; -import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.io.Connection; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Connector; @@ -30,16 +30,98 @@ /** * Debug Handler. + *

* A lightweight debug handler that can be used in production code. * Details of the request and response are written to an output stream * and the current thread name is updated with information that will link * to the details in that output. + *

+ *

+ * Note that due to async processing, the logging of request processing may + * appear out of order. + *

*/ public class DebugHandler extends Handler.Wrapper implements Connection.Listener { - private final DateCache _date = new DateCache("HH:mm:ss", Locale.US); + private static final DateCache __date = new DateCache("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); private OutputStream _out; private PrintStream _print; + private boolean _showHeaders; + private final String _attr = String.format("__R%s@%x", this.getClass().getSimpleName(), System.identityHashCode(this)); + + /** + * Callback class used to manage possibly async handling by downstream handlers. + * The + */ + private class HandlingCallback extends Callback.Nested + { + private final Request _request; + private final Response _response; + private final AtomicBoolean _completing = new AtomicBoolean(false); + + private HandlingCallback(Callback callback, Request request, Response response) + { + super(callback); + _request = request; + _response = response; + } + + /** + * Called when the handler chain has completed. We log either the completion of the request + * or an in-progress message if the request is still being processed asynchronously (ie this + * callback's {@link #succeeded()} or {@link #failed(Throwable)} methods have not yet been called). + */ + private void onHandlingCompleted(Throwable throwable) + { + if (_completing.compareAndSet(false, true)) + { + logContinuation(); + } + else + { + logCompletion(throwable); + } + } + + /** + * Called when this callback completes, either with success or failure. We output a + * request completion log message only in the case where we have been called back + * asynchronously after the handler chain has finished. + * @param throwable null if the handling succeeded or otherwise the failure that occurred + */ + private void onCallbackCompleted(Throwable throwable) + { + //If we can set the value to true, we know that handling has not yet finalized and onHandlingCompleted(Throwable) + //will be called and will log the completion message. On the other hand, if we are not able to set it, then this means that + //handling has already finished, so we must log the completion message. + if (!_completing.compareAndSet(false, true)) + logCompletion(throwable); + } + + private void logCompletion(Throwable throwable) + { + log("<< r=%s async=false %d %s%n%s", findRequestName(_request), _response.getStatus(), (throwable == null ? "" : throwable.toString()), _response.getHeaders()); + } + + private void logContinuation() + { + log("|| r=%s async=true", findRequestName(_request)); + } + + @Override + public void succeeded() + { + onCallbackCompleted(null); + super.succeeded(); + } + + @Override + public void failed(Throwable x) + { + onCallbackCompleted(x); + super.failed(x); + } + } public DebugHandler() { @@ -51,42 +133,95 @@ public DebugHandler(Handler handler) super(handler); } + public boolean isShowHeaders() + { + return _showHeaders; + } + + public void setShowHeaders(boolean showHeaders) + { + _showHeaders = showHeaders; + } + @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { Thread thread = Thread.currentThread(); String name = thread.getName() + ":" + request.getHttpURI(); + boolean willHandle = false; + Throwable ex = null; + String rname = findRequestName(request); + HandlingCallback handlingCallback = new HandlingCallback(callback, request, response); - String ex = null; try { - print(name, "REQUEST " + Request.getRemoteAddr(request) + - " " + request.getMethod() + - " " + request.getHeaders().get("Cookie") + - "; " + request.getHeaders().get("User-Agent")); + String headers = _showHeaders ? ("\n" + request.getHeaders().toString()) : ""; + + log(">> r=%s %s %s %s %s %s", + rname, + request.getMethod(), + request.getHttpURI(), + request.getConnectionMetaData().getProtocol(), + request.getConnectionMetaData(), + headers); thread.setName(name); - return getHandler().handle(request, response, callback); + willHandle = getHandler().handle(request, response, handlingCallback); + return willHandle; } catch (Throwable x) { - ex = x + ":" + x.getCause(); + ex = x; throw x; } finally { - // TODO this should be done in a completion event - print(name, "RESPONSE " + response.getStatus() + (ex == null ? "" : ("/" + ex)) + " " + response.getHeaders().get(HttpHeader.CONTENT_TYPE)); + if (!willHandle) + { + //Log that the request was not going to be handled + log("!! r=%s not handled", rname); + } + else + { + handlingCallback.onHandlingCompleted(ex); + } } } - private void print(String name, String message) + protected void log(String format, Object... arg) { + if (!isRunning()) + return; + + String s = String.format(format, arg); + long now = System.currentTimeMillis(); - final String d = _date.format(now); - final int ms = (int)(now % 1000); + long ms = now % 1000; + if (_print != null) + _print.printf("%s.%03d:%s%n", __date.format(now), ms, s); + } + + protected String findRequestName(Request request) + { + if (request == null) + return null; - _print.println(d + (ms > 99 ? "." : (ms > 9 ? ".0" : ".00")) + ms + ":" + name + " " + message); + try + { + String n = (String)request.getAttribute(_attr); + if (n == null) + { + n = String.format("%s@%x", request.getHttpURI(), request.hashCode()); + request.setAttribute(_attr, n); + } + return n; + } + catch (IllegalStateException e) + { + // TODO can we avoid creating and catching this exception? see #8024 + // Handle the case when the request has already been completed + return String.format("%s@%x", request.getHttpURI(), request.hashCode()); + } } @Override @@ -119,6 +254,7 @@ protected void doStop() throws Exception /** * Get the out. + * * @return the out */ public OutputStream getOutputStream() @@ -128,6 +264,7 @@ public OutputStream getOutputStream() /** * Set the out to set. + * * @param out the out to set */ public void setOutputStream(OutputStream out) @@ -138,12 +275,12 @@ public void setOutputStream(OutputStream out) @Override public void onOpened(Connection connection) { - print(Thread.currentThread().getName(), "OPENED " + connection.toString()); + log("%s OPENED %s", Thread.currentThread().getName(), connection.toString()); } @Override public void onClosed(Connection connection) { - print(Thread.currentThread().getName(), "CLOSED " + connection.toString()); + log("%s CLOSED %s", Thread.currentThread().getName(), connection.toString()); } } diff --git a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java index 63c32f0d45cf..353a410bcc77 100644 --- a/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java +++ b/jetty-ee10/jetty-ee10-webapp/src/main/java/org/eclipse/jetty/ee10/webapp/WebAppContext.java @@ -214,7 +214,11 @@ public void initializeDefaults(Map properties) switch (property) { - case Deployable.WAR -> setWar(value); + case Deployable.WAR -> + { + if (getWar() == null) + setWar(value); + } case Deployable.TEMP_DIR -> setTempDirectory(IO.asFile(value)); case Deployable.CONFIGURATION_CLASSES -> setConfigurationClasses(value == null ? null : value.split(",")); case Deployable.CONTAINER_SCAN_JARS -> setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN, value); diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java index 6f6bfc2366ff..077f10c6a6b8 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DemoModulesTests.java @@ -14,6 +14,7 @@ package org.eclipse.jetty.tests.distribution; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.NoSuchElementException; @@ -699,4 +700,50 @@ public void testJettyDemo(String env) throws Exception } } + @ParameterizedTest + @MethodSource("provideEnvironmentsToTest") + public void testDebugLogModule(String env) throws Exception + { + Path jettyBase = newTestJettyBaseDirectory(); + String jettyVersion = System.getProperty("jettyVersion"); + JettyHomeTester distribution = JettyHomeTester.Builder.newInstance() + .jettyVersion(jettyVersion) + .jettyBase(jettyBase) + .build(); + + int httpPort = Tester.freePort(); + int sslPort = Tester.freePort(); + + String[] argsConfig = { + "--add-modules=http," + toEnvironment("demos", env) + ",debuglog" + }; + + try (JettyHomeTester.Run runConfig = distribution.start(argsConfig)) + { + assertTrue(runConfig.awaitFor(START_TIMEOUT, TimeUnit.SECONDS)); + assertEquals(0, runConfig.getExitValue()); + + String[] argsStart = { + "jetty.http.port=" + httpPort, + "jetty.ssl.port=" + sslPort, + "jetty.server.dumpAfterStart=true" + }; + + try (JettyHomeTester.Run runStart = distribution.start(argsStart)) + { + assertTrue(runStart.awaitConsoleLogsFor("Started oejs.Server@", START_TIMEOUT, TimeUnit.SECONDS)); + startHttpClient(); + String baseURI = "http://localhost:%d/%s-test".formatted(httpPort, env); + + ContentResponse response = client.POST(baseURI + "/dump/info").send(); + assertEquals(HttpStatus.OK_200, response.getStatus(), new ResponseDetails(response)); + Path jettyLogs = jettyBase.resolve("logs"); + assertTrue(Files.isDirectory(jettyLogs)); + try (Stream files = Files.list(jettyLogs)) + { + assertTrue(files.anyMatch(p -> p.toFile().getName().endsWith(".debug.log"))); + } + } + } + } }