This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch jbFixRestNpe
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/jbFixRestNpe by this push:
new bbf4b24b1 Re-add LogResource to Jetty microservice.
bbf4b24b1 is described below
commit bbf4b24b1ed8fb83f477da703f9553b30937b42e
Author: JamesBognar <[email protected]>
AuthorDate: Sun Jul 10 07:15:24 2022 -0400
Re-add LogResource to Jetty microservice.
---
.../java/org/apache/juneau/internal/Cache.java | 2 +-
.../04.jrs.HttpParts/03.jrs.DefaultParts.html | 48 ++-
juneau-doc/src/main/javadoc/overview.html | 52 +++-
.../src/main/javadoc/resources/fragments/toc.html | 46 +--
.../juneau-examples-rest-jetty.cfg | 67 ++++
.../juneau/examples/rest/HtmlBeansResource.java | 1 -
.../apache/juneau/examples/rest/RootResources.java | 1 +
.../org/apache/juneau/microservice/LogConfig.java | 204 +++++++++++++
.../apache/juneau/microservice/Microservice.java | 79 ++++-
.../microservice/resources/LogEntryFormatter.java | 268 ++++++++++++++++
.../juneau/microservice/resources/LogParser.java | 224 ++++++++++++++
.../microservice/resources/LogsResource.java | 338 +++++++++++++++++++++
12 files changed, 1300 insertions(+), 30 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/Cache.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/Cache.java
index afa4f58bf..ad2785479 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/Cache.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/Cache.java
@@ -68,7 +68,7 @@ public class Cache<K,V> {
this.type = type;
disabled = env("juneau.cache.disable", false);
maxSize = env("juneau.cache.maxSize", 1000);
- logOnExit = env("juneau.cache.logOnExit", true);
+ logOnExit = env("juneau.cache.logOnExit", false);
}
/**
diff --git
a/juneau-doc/docs/Topics/08.juneau-rest-server/04.jrs.HttpParts/03.jrs.DefaultParts.html
b/juneau-doc/docs/Topics/08.juneau-rest-server/04.jrs.HttpParts/03.jrs.DefaultParts.html
index e7c1d8c46..06ae25b3f 100644
---
a/juneau-doc/docs/Topics/08.juneau-rest-server/04.jrs.HttpParts/03.jrs.DefaultParts.html
+++
b/juneau-doc/docs/Topics/08.juneau-rest-server/04.jrs.HttpParts/03.jrs.DefaultParts.html
@@ -17,7 +17,53 @@
<div class='topic'>
<p>
- The following annotations are provided for specifying default
part values for requests and responses:
+ By default, HTTP parts that don't have value (such as missing
query parameters) end up with null
+ values:
+ </p>
+ <p class='bjava'>
+ | <ja>@RestPost</ja>(<js>"/example"</js>)
+ | <jk>public</jk> String doGetExample1(
+ | <ja>@Query</ja>(<js>"p1"</js>) <jk>int</jk>
<jv>p1</jv>,
+ | <ja>@FormData</ja>(<js>"f1"</js>) MyBean
<jv>f1</jv>,
+ | <ja>@Header</ja>(<js>"Accept-Language"</js>)
AcceptLanguage <jv>h1</jv>
+ | ) {
+ | <jk>if</jk> (<jv>p1</jv> == <jk>null</jk>)
<jv>p1</jv> = -1;
+ | <jk>if</jk> (<jv>f1</jv> == <jk>null</jk>)
<jv>f1</jv> = <jsf>DEFAULT_BEAN</jsf>;
+ | <jk>if</jk> (<jv>h1</jv> == <jk>null</jk>)
<jv>h1</jv> = AcceptLanguage.<jsm>of</jsm>(<js>"en"</js>);
+ | }
+ </p>
+ <p>
+ You have several options to provide default values for HTTP
parts. The most common is to simply
+ use {@link java.util.Optional} parameters and handle default
values programmatically:
+ </p>
+ <p class='bjava'>
+ | <ja>@RestPost</ja>(<js>"/example"</js>)
+ | <jk>public</jk> String doGetExample1(
+ | <ja>@Query</ja>(<js>"p1"</js>)
Optional<Integer> <jv>p1</jv>,
+ | <ja>@FormData</ja>(<js>"f1"</js>)
Optional<MyBean> <jv>f1</jv>,
+ | <ja>@Header</ja>(<js>"Accept-Language"</js>)
Optional<AcceptLanguage> <jv>h1</jv>
+ | ) {
+ | <jk>int</jk> <jv>_p1</jv> =
<jv>p1</jv>.orElse(-1);
+ | Bean <jv>_f1</jv> =
<jv>f1</jv>.orElse(<jsf>DEFAULT_BEAN</jsf>);
+ | AcceptLanguage <jv>_h1</jv> =
<jv>h1</jv>.orElse(AcceptLanguage.<jsm>of</jsm>(<js>"en"</js>));
+ | }
+ </p>
+ <p>
+ You can also specify default values on the annotations:
+ </p>
+ <p class='bjava'>
+ | <ja>@RestPost</ja>(<js>"/example"</js>)
+ | <jk>public</jk> String doGetExample1(
+ | <ja>@Query</ja>(name=<js>"p1"</js>,
def=<js>"-1"</js>) <jk>int</jk> <jv>p1</jv>,
+ | <ja>@FormData</ja>(name=<js>"f1"</js>,
def=<js>"foo=bar,baz=qux"</js>) MyBean <jv>f1</jv>,
+ |
<ja>@Header</ja>(name=<js>"Accept-Language"</js>, def=<js>"en"</js>)
AcceptLanguage <jv>lang</jv>
+ | ) {
+ | ...
+ | }
+ </p>
+ <p>
+ A third option is to specify default values via the {@link
oajr.annotation.Rest} and
+ {@link oajr.annotation.RestOp} annotations.
</p>
<ul class='javatree'>
<li class='ja'>{@link oajr.annotation.Rest}
diff --git a/juneau-doc/src/main/javadoc/overview.html
b/juneau-doc/src/main/javadoc/overview.html
index a754a9902..74b303c0c 100644
--- a/juneau-doc/src/main/javadoc/overview.html
+++ b/juneau-doc/src/main/javadoc/overview.html
@@ -18195,7 +18195,53 @@
<div class='topic'><!-- START: 8.4.3 -
juneau-rest-server.jrs.HttpParts.jrs.DefaultParts -->
<div class='topic'>
<p>
- The following annotations are provided for specifying default
part values for requests and responses:
+ By default, HTTP parts that don't have value (such as missing
query parameters) end up with null
+ values:
+ </p>
+ <p class='bjava'>
+ <ja>@RestPost</ja>(<js>"/example"</js>)
+ <jk>public</jk> String doGetExample1(
+ <ja>@Query</ja>(<js>"p1"</js>) <jk>int</jk> <jv>p1</jv>,
+ <ja>@FormData</ja>(<js>"f1"</js>) MyBean <jv>f1</jv>,
+ <ja>@Header</ja>(<js>"Accept-Language"</js>) AcceptLanguage
<jv>h1</jv>
+ ) {
+ <jk>if</jk> (<jv>p1</jv> == <jk>null</jk>) <jv>p1</jv> = -1;
+ <jk>if</jk> (<jv>f1</jv> == <jk>null</jk>) <jv>f1</jv> =
<jsf>DEFAULT_BEAN</jsf>;
+ <jk>if</jk> (<jv>h1</jv> == <jk>null</jk>) <jv>h1</jv> =
AcceptLanguage.<jsm>of</jsm>(<js>"en"</js>);
+ }
+ </p>
+ <p>
+ You have several options to provide default values for HTTP
parts. The most common is to simply
+ use {@link java.util.Optional} parameters and handle default
values programmatically:
+ </p>
+ <p class='bjava'>
+ <ja>@RestPost</ja>(<js>"/example"</js>)
+ <jk>public</jk> String doGetExample1(
+ <ja>@Query</ja>(<js>"p1"</js>) Optional<Integer>
<jv>p1</jv>,
+ <ja>@FormData</ja>(<js>"f1"</js>) Optional<MyBean>
<jv>f1</jv>,
+ <ja>@Header</ja>(<js>"Accept-Language"</js>)
Optional<AcceptLanguage> <jv>h1</jv>
+ ) {
+ <jk>int</jk> <jv>_p1</jv> = <jv>p1</jv>.orElse(-1);
+ Bean <jv>_f1</jv> = <jv>f1</jv>.orElse(<jsf>DEFAULT_BEAN</jsf>);
+ AcceptLanguage <jv>_h1</jv> =
<jv>h1</jv>.orElse(AcceptLanguage.<jsm>of</jsm>(<js>"en"</js>));
+ }
+ </p>
+ <p>
+ You can also specify default values on the annotations:
+ </p>
+ <p class='bjava'>
+ <ja>@RestPost</ja>(<js>"/example"</js>)
+ <jk>public</jk> String doGetExample1(
+ <ja>@Query</ja>(name=<js>"p1"</js>, def=<js>"-1"</js>)
<jk>int</jk> <jv>p1</jv>,
+ <ja>@FormData</ja>(name=<js>"f1"</js>,
def=<js>"foo=bar,baz=qux"</js>) MyBean <jv>f1</jv>,
+ <ja>@Header</ja>(name=<js>"Accept-Language"</js>,
def=<js>"en"</js>) AcceptLanguage <jv>lang</jv>
+ ) {
+ ...
+ }
+ </p>
+ <p>
+ A third option is to specify default values via the {@link
org.apache.juneau.rest.annotation.Rest} and
+ {@link org.apache.juneau.rest.annotation.RestOp} annotations.
</p>
<ul class='javatree'>
<li class='ja'>{@link org.apache.juneau.rest.annotation.Rest}
@@ -27731,7 +27777,7 @@
<cc># Various look-and-feel settings used in the BasicRestConfig
interface.</cc>
<ck>headerIcon</ck> = <cv>servlet:/htdocs/images/juneau.png</cv>
- v <ck>headerLink</ck> =
<cv>http://juneau.apache.org</cv>
+ <ck>headerLink</ck> = <cv>http://juneau.apache.org</cv>
<ck>footerIcon</ck> = <cv>servlet:/htdocs/images/asf.png</cv>
<ck>footerLink</ck> = <cv>http://www.apache.org</cv>
<ck>favicon</ck> = <cv>$C{REST/headerIcon}</cv>
@@ -27743,7 +27789,7 @@
<cv><a href='$U{$C{REST/footerLink}}'>
<img src='$U{$C{REST/footerIcon}}'
style='float:right;padding-right:20px;height:32px'/>
</a></cv>
- </p>
+ </p>
</ul>
<p>
At this point, you're ready to start the microservice from your
workspace.
diff --git a/juneau-doc/src/main/javadoc/resources/fragments/toc.html
b/juneau-doc/src/main/javadoc/resources/fragments/toc.html
index cf81c7cff..143c5cf22 100644
--- a/juneau-doc/src/main/javadoc/resources/fragments/toc.html
+++ b/juneau-doc/src/main/javadoc/resources/fragments/toc.html
@@ -328,34 +328,34 @@
<ol>
<li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-rest-mock.jrm.MockRestClient'>MockRestClient</a><span
class='update'>created: 8.2.0, updated: <b>9.0.0</b></span></p>
</ol>
- <li><p class='toc2'><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core'>juneau-microservice-core</a><span
class='update'>created: 8.1.0, deprecated: 8.1.2</span></p>
+ <li><p class='toc2'><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core'>juneau-microservice-core</a><span
class='update'>created: 8.1.0</span></p>
<ol>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Overview'>Microservice
Overview</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.LifecycleMethods'>Lifecycle
Methods</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Args'>Args</a><span
class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Manifest'>Manifest</a><span
class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Config'>Config</a><span
class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.SystemProperties'>System
properties</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.VarResolver'>VarResolver</a><span
class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.ConsoleCommands'>Console
Commands</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Listeners'>Listeners</a><span
class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Overview'>Microservice
Overview</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.LifecycleMethods'>Lifecycle
Methods</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Args'>Args</a><span
class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Manifest'>Manifest</a><span
class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Config'>Config</a><span
class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.SystemProperties'>System
properties</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.VarResolver'>VarResolver</a><span
class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.ConsoleCommands'>Console
Commands</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-core.jmc.Listeners'>Listeners</a><span
class='update'>created: 8.0.0</span></p>
</ol>
- <li><p class='toc2'><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty'>juneau-microservice-jetty</a><span
class='update'>created: 8.1.0, deprecated: 8.1.2</span></p>
+ <li><p class='toc2'><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty'>juneau-microservice-jetty</a><span
class='update'>created: 8.1.0</span></p>
<ol>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.Overview'>Overview</a><span
class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.LifecycleMethods'>Lifecycle
Methods</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.ResourceClasses'>Resource
Classes</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.PredefinedResourceClasses'>Predefined
Resource Classes</a><span class='update'>created: 8.0.0, deprecated:
8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.Config'>Config</a><span
class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.JettyXml'>Jetty.xml
file</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.UiCustomization'>UI
Customization</a><span class='update'>created: 8.0.0, deprecated:
8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.Extending'>Extending
JettyMicroservice</a><span class='update'>created: 8.0.0, deprecated:
8.1.2</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.Overview'>Overview</a><span
class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.LifecycleMethods'>Lifecycle
Methods</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.ResourceClasses'>Resource
Classes</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.PredefinedResourceClasses'>Predefined
Resource Classes</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.Config'>Config</a><span
class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.JettyXml'>Jetty.xml
file</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.UiCustomization'>UI
Customization</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#juneau-microservice-jetty.jmj.Extending'>Extending
JettyMicroservice</a><span class='update'>created: 8.0.0</span></p>
</ol>
- <li><p class='toc2'><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice'>my-jetty-microservice</a><span
class='update'>created: 8.1.0, deprecated: 8.1.2</span></p>
+ <li><p class='toc2'><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice'>my-jetty-microservice</a><span
class='update'>created: 8.1.0</span></p>
<ol>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice.mjm.Installing'>Installing in
Eclipse</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice.mjm.Running'>Running in
Eclipse</a><span class='update'>created: 8.0.0, deprecated: 8.1.2</span></p>
- <li><p><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice.mjm.Building'>Building and Running
from Command-Line</a><span class='update'>created: 8.0.0, deprecated:
8.1.2</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice.mjm.Installing'>Installing in
Eclipse</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice.mjm.Running'>Running in
Eclipse</a><span class='update'>created: 8.0.0</span></p>
+ <li><p><a class='doclink'
href='{OVERVIEW_URL}#my-jetty-microservice.mjm.Building'>Building and Running
from Command-Line</a><span class='update'>created: 8.0.0</span></p>
</ol>
<li><p class='toc2'><a class='doclink'
href='{OVERVIEW_URL}#my-springboot-microservice'>my-springboot-microservice</a><span
class='update'>created: 8.0.0</span></p>
<ol>
diff --git
a/juneau-examples/juneau-examples-rest-jetty/juneau-examples-rest-jetty.cfg
b/juneau-examples/juneau-examples-rest-jetty/juneau-examples-rest-jetty.cfg
index f11c0cd1b..e09a2d53b 100755
--- a/juneau-examples/juneau-examples-rest-jetty/juneau-examples-rest-jetty.cfg
+++ b/juneau-examples/juneau-examples-rest-jetty/juneau-examples-rest-jetty.cfg
@@ -89,6 +89,73 @@ commands =
org.apache.juneau.microservice.console.ConfigCommand,
org.apache.juneau.examples.rest.command.EchoCommand
+#=======================================================================================================================
+# Logger settings
+#-----------------------------------------------------------------------------------------------------------------------
+# See FileHandler Java class for details.
+#=======================================================================================================================
+[Logging]
+
+# The directory where to create the log file.
+# Default is "."
+logDir = ./target/logs
+
+# The name of the log file to create for the main logger.
+# The logDir and logFile make up the pattern that's passed to the FileHandler
+# constructor.
+# If value is not specified, then logging to a file will not be set up.
+logFile = microservice.%g.log
+
+# Whether to append to the existing log file or create a new one.
+append = false
+
+# The SimpleDateFormat format to use for dates.
+dateFormat = yyyy.MM.dd hh:mm:ss
+
+# The log message format.
+# The value can contain any of the following variables:
+# {date} - The date, formatted per dateFormat.
+# {class} - The class name.
+# {method} - The method name.
+# {logger} - The logger name.
+# {level} - The log level name.
+# {msg} - The log message.
+# {threadid} - The thread ID.
+# {exception} - The localized exception message.
+format = [{date} {level}] {msg}%n
+
+# The maximum log file size.
+# Suffixes available for numbers.
+# See Config.getInt(String,int) for details.
+limit = 1M
+
+# Max number of log files.
+count = 5
+
+# Default log levels.
+# Format is lax-JSON.
+# Keys are logger names.
+# Values are serialized Level POJOs (SEVERE, WARNING, INFO, CONFIG, FINE,
FINER, FINEST)
+levels =
+ {
+ '': 'WARNING',
+ org.apache.juneau: 'WARNING',
+ org.eclipse.jetty: 'WARNING'
+ }
+
+# Only print unique stack traces once and then refer to them by a simple 8
character hash identifier.
+# Useful for preventing log files from filling up with duplicate stack traces.
+useStackTraceHashes = true
+
+# The default level for the console logger.
+# Values are serialized Level POJOs (SEVERE, WARNING, INFO, CONFIG, FINE,
FINER, FINEST)
+consoleLevel = WARNING
+
+# The default level for the file logger.
+# Values are serialized Level POJOs (SEVERE, WARNING, INFO, CONFIG, FINE,
FINER, FINEST)
+# Default is INFO.
+fileLevel = INFO
+
#=======================================================================================================================
# System properties
#-----------------------------------------------------------------------------------------------------------------------
diff --git
a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HtmlBeansResource.java
b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HtmlBeansResource.java
index f59773a0a..1d3800fc4 100644
---
a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HtmlBeansResource.java
+++
b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/HtmlBeansResource.java
@@ -54,7 +54,6 @@ import org.apache.juneau.rest.widget.*;
},
asideFloat="RIGHT"
)
-@SuppressWarnings("javadoc")
public class HtmlBeansResource extends BasicRestObject implements
BasicUniversalConfig {
@SuppressWarnings("unused")
diff --git
a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
index 2cbb0f8c6..0550b02be 100644
---
a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
+++
b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
@@ -39,6 +39,7 @@ import org.apache.juneau.serializer.annotation.*;
UtilityBeansResource.class,
HtmlBeansResource.class,
ConfigResource.class,
+ LogsResource.class,
ShutdownResource.class
}
)
diff --git
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/LogConfig.java
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/LogConfig.java
new file mode 100644
index 000000000..e558bc143
--- /dev/null
+++
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/LogConfig.java
@@ -0,0 +1,204 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.microservice;
+
+import java.util.*;
+import java.util.logging.*;
+import java.util.logging.Formatter;
+
+import org.apache.juneau.microservice.resources.*;
+
+/**
+ * Can be used for configuration of simple logging in the microservice.
+ *
+ * <p>
+ * Instances of this class can be created using {@link #create()} and passing
the result to
+ * {@link Microservice.Builder#logConfig(LogConfig)}.
+ *
+ * <p>
+ * These values override values specified in the <js>"Logging"</js>
configuration section.
+ */
+public class LogConfig {
+
+ String logFile, logDir;
+ Boolean append;
+ Integer limit, count;
+ Level fileLevel, consoleLevel;
+ Map<String,Level> levels = new LinkedHashMap<>();
+ Formatter formatter;
+
+ LogConfig() {}
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The log config to copy from.
+ */
+ protected LogConfig(LogConfig copyFrom) {
+ this.logFile = copyFrom.logFile;
+ this.logDir = copyFrom.logDir;
+ this.append = copyFrom.append;
+ this.limit = copyFrom.limit;
+ this.count = copyFrom.count;
+ this.fileLevel = copyFrom.fileLevel;
+ this.consoleLevel = copyFrom.consoleLevel;
+ this.levels = new LinkedHashMap<>(copyFrom.levels);
+ this.formatter = copyFrom.formatter;
+ }
+
+ /**
+ * Creates a copy of this log configuration.
+ *
+ * @return A new copy of this log configuration.
+ */
+ public LogConfig copy() {
+ return new LogConfig(this);
+ }
+
+ /**
+ * Creates a new instance of this config.
+ *
+ * @return A new instance of this config.
+ */
+ public static LogConfig create() {
+ return new LogConfig();
+ }
+
+ /**
+ * Returns the name of the log file on the file system to store the log
file for this microservice.
+ *
+ * <p>
+ * This overrides the configuration value <js>"Logging/logFile"</js>.
+ * If not specified, no file system logging will be used.
+ *
+ * @param logFile The log file.
+ * @return This object (for method chaining).
+ */
+ public LogConfig logFile(String logFile) {
+ this.logFile = logFile;
+ return this;
+ }
+
+ /**
+ * The location of the log directory to create the log file.
+ *
+ * <p>
+ * This overrides the configuration value <js>"Logging/logDir"</js>.
+ * If not specified, uses the JVM working directory.
+ *
+ * @param logDir The log directory location as a path relative to the
working directory.
+ * @return This object (for method chaining).
+ */
+ public LogConfig logDir(String logDir) {
+ this.logDir = logDir;
+ return this;
+ }
+
+ /**
+ * The log entry formatter.
+ *
+ * <p>
+ * If not specified, uses the following values pulled from the
configuration to construct a {@link LogEntryFormatter}:
+ * <ul>
+ * <li><js><js>"Logging/format"</js> (default is <js>"[{date}
{level}] {msg}%n"</js>)
+ * <li><js><js>"Logging/dateFormat"</js> (default is
<js>"yyyy.MM.dd hh:mm:ss"</js>)
+ * <li><js><js>"Logging/useStackTraceHashes"</js> (default is
<jk>false</jk>)
+ * </ul>
+ *
+ *
+ * @param formatter The log entry formatter.
+ * @return This object (for method chaining).
+ * @see LogEntryFormatter
+ */
+ public LogConfig formatter(Formatter formatter) {
+ this.formatter = formatter;
+ return this;
+ }
+
+ /**
+ * Specified append mode for the log file.
+ *
+ * @return This object (for method chaining).
+ */
+ public LogConfig append() {
+ this.append = true;
+ return this;
+ }
+
+ /**
+ * The maximum number of bytes to write to any one log file.
+ *
+ * @param limit The number of bytes.
+ * @return This object (for method chaining).
+ */
+ public LogConfig limit(int limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ /**
+ * The number of log files to use.
+ *
+ * @param count The number of log files.
+ * @return This object (for method chaining).
+ */
+ public LogConfig count(int count) {
+ this.count = count;
+ return this;
+ }
+
+ /**
+ * The default logging level for the log file.
+ *
+ * @param fileLevel The logging level.
+ * @return This object (for method chaining).
+ */
+ public LogConfig fileLevel(Level fileLevel) {
+ this.fileLevel = fileLevel;
+ return this;
+ }
+
+ /**
+ * The default logging level for the console.
+ *
+ * @param consoleLevel The logging level.
+ * @return This object (for method chaining).
+ */
+ public LogConfig consoleLevel(Level consoleLevel) {
+ this.consoleLevel = consoleLevel;
+ return this;
+ }
+
+ /**
+ * Default logging levels for loggers.
+ *
+ * @param levels A map of logger names to logger levels.
+ * @return This object (for method chaining).
+ */
+ public LogConfig levels(Map<String,Level> levels) {
+ this.levels.putAll(levels);
+ return this;
+ }
+
+ /**
+ * Default logging level for logger.
+ *
+ * @param logger Logger name.
+ * @param level Logger level.
+ * @return This object (for method chaining).
+ */
+ public LogConfig level(String logger, Level level) {
+ this.levels.put(logger, level);
+ return this;
+ }
+}
diff --git
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
index ea8d48feb..5319ed20b 100755
---
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
+++
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
@@ -14,8 +14,11 @@ package org.apache.juneau.microservice;
import static org.apache.juneau.internal.ClassUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;
+import static org.apache.juneau.internal.FileUtils.*;
import static org.apache.juneau.internal.IOUtils.*;
+import static org.apache.juneau.internal.ObjectUtils.*;
import static org.apache.juneau.internal.StringUtils.*;
+
import java.io.*;
import java.io.Console;
import java.net.*;
@@ -25,6 +28,7 @@ import java.util.*;
import java.util.concurrent.*;
import java.util.jar.*;
import java.util.logging.*;
+import java.util.logging.Formatter;
import org.apache.juneau.*;
import org.apache.juneau.collections.*;
@@ -36,6 +40,7 @@ import org.apache.juneau.config.vars.*;
import org.apache.juneau.cp.*;
import org.apache.juneau.internal.*;
import org.apache.juneau.microservice.console.*;
+import org.apache.juneau.microservice.resources.*;
import org.apache.juneau.parser.ParseException;
import org.apache.juneau.svl.*;
import org.apache.juneau.svl.vars.*;
@@ -140,6 +145,7 @@ public class Microservice implements ConfigEventListener {
Args args;
ManifestFile manifest;
Logger logger;
+ LogConfig logConfig;
Config config;
String configName;
ConfigStore configStore;
@@ -167,6 +173,7 @@ public class Microservice implements ConfigEventListener {
this.manifest = copyFrom.manifest;
this.logger = copyFrom.logger;
this.configName = copyFrom.configName;
+ this.logConfig = copyFrom.logConfig == null ? null :
copyFrom.logConfig.copy();
this.consoleEnabled = copyFrom.consoleEnabled;
this.configBuilder = copyFrom.configBuilder;
this.varResolver = copyFrom.varResolver;
@@ -283,6 +290,9 @@ public class Microservice implements ConfigEventListener {
/**
* Specifies the logger used by the microservice and returned
by the {@link Microservice#getLogger()} method.
*
+ * <p>
+ * Calling this method overrides the default logging mechanism
controlled by the {@link #logConfig(LogConfig)} method.
+ *
* @param logger The logger to use for logging microservice
messages.
* @return This object.
*/
@@ -291,6 +301,23 @@ public class Microservice implements ConfigEventListener {
return this;
}
+ /**
+ * Specifies logging instructions for the microservice.
+ *
+ * <p>
+ * If not specified, the values are taken from the
<js>"Logging"</js> section of the configuration.
+ *
+ * <p>
+ * This method is ignored if {@link #logger(Logger)} is used to
set the microservice logger.
+ *
+ * @param logConfig The log configuration.
+ * @return This object.
+ */
+ public Builder logConfig(LogConfig logConfig) {
+ this.logConfig = logConfig;
+ return this;
+ }
+
/**
* Specifies the config for initializing this microservice.
*
@@ -522,6 +549,7 @@ public class Microservice implements ConfigEventListener {
final Messages messages = Messages.of(Microservice.class);
+ private final Builder builder;
private final Args args;
private final Config config;
private final ManifestFile manifest;
@@ -535,7 +563,7 @@ public class Microservice implements ConfigEventListener {
final File workingDir;
private final String configName;
- private volatile Logger logger = Logger.getGlobal();
+ private volatile Logger logger;
/**
* Constructor.
@@ -544,8 +572,10 @@ public class Microservice implements ConfigEventListener {
* @throws IOException Problem occurred reading file.
* @throws ParseException Malformed input encountered.
*/
+ @SuppressWarnings("resource")
protected Microservice(Builder builder) throws IOException,
ParseException {
setInstance(this);
+ this.builder = builder.copy();
this.workingDir = builder.workingDir;
this.configName = builder.configName;
@@ -734,6 +764,53 @@ public class Microservice implements ConfigEventListener {
for (String key : spKeys)
System.setProperty(key,
config.get("SystemProperties/"+key).orElse(null));
+ //
--------------------------------------------------------------------------------
+ // Initialize logging.
+ //
--------------------------------------------------------------------------------
+ this.logger = builder.logger;
+ LogConfig logConfig = builder.logConfig != null ?
builder.logConfig : new LogConfig();
+ if (this.logger == null) {
+ LogManager.getLogManager().reset();
+ this.logger = Logger.getLogger("");
+ String logFile = firstNonNull(logConfig.logFile,
config.get("Logging/logFile").orElse(null));
+
+ if (isNotEmpty(logFile)) {
+ String logDir = firstNonNull(logConfig.logDir,
config.get("Logging/logDir").orElse("."));
+ File logDirFile = resolveFile(logDir);
+ mkdirs(logDirFile, false);
+ logDir = logDirFile.getAbsolutePath();
+ System.setProperty("juneau.logDir", logDir);
+
+ boolean append = firstNonNull(logConfig.append,
config.get("Logging/append").asBoolean().orElse(false));
+ int limit = firstNonNull(logConfig.limit,
config.get("Logging/limit").asInteger().orElse(1024*1024));
+ int count = firstNonNull(logConfig.count,
config.get("Logging/count").asInteger().orElse(1));
+
+ FileHandler fh = new FileHandler(logDir + '/' +
logFile, limit, count, append);
+
+ Formatter f = logConfig.formatter;
+ if (f == null) {
+ String format =
config.get("Logging/format").orElse("[{date} {level}] {msg}%n");
+ String dateFormat =
config.get("Logging/dateFormat").orElse("yyyy.MM.dd hh:mm:ss");
+ boolean useStackTraceHashes =
config.get("Logging/useStackTraceHashes").asBoolean().orElse(false);
+ f = new LogEntryFormatter(format,
dateFormat, useStackTraceHashes);
+ }
+ fh.setFormatter(f);
+ fh.setLevel(firstNonNull(logConfig.fileLevel,
config.get("Logging/fileLevel").as(Level.class).orElse(Level.INFO)));
+ logger.addHandler(fh);
+
+ ConsoleHandler ch = new ConsoleHandler();
+
ch.setLevel(firstNonNull(logConfig.consoleLevel,
config.get("Logging/consoleLevel").as(Level.class).orElse(Level.WARNING)));
+ ch.setFormatter(f);
+ logger.addHandler(ch);
+ }
+ }
+
+ JsonMap loggerLevels =
config.get("Logging/levels").as(JsonMap.class).orElseGet(JsonMap::new);
+ for (String l : loggerLevels.keySet())
+ Logger.getLogger(l).setLevel(loggerLevels.get(l,
Level.class));
+ for (String l : logConfig.levels.keySet())
+ Logger.getLogger(l).setLevel(logConfig.levels.get(l));
+
return this;
}
diff --git
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
new file mode 100644
index 000000000..0b8d85fb5
--- /dev/null
+++
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
@@ -0,0 +1,268 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.microservice.resources;
+
+import static org.apache.juneau.internal.CollectionUtils.*;
+
+import java.text.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.*;
+import java.util.logging.*;
+import java.util.logging.Formatter;
+import java.util.regex.*;
+
+import org.apache.juneau.internal.*;
+
+/**
+ * Log entry formatter.
+ *
+ * <p>
+ * Uses three simple parameter for configuring log entry formats:
+ * <ul class='spaced-list'>
+ * <li>
+ * <c>dateFormat</c> - A {@link SimpleDateFormat} string
describing the format for dates.
+ * <li>
+ * <c>format</c> - A string with <c>{...}</c> replacement
variables representing predefined fields.
+ * <li>
+ * <c>useStackTraceHashes</c> - A setting that causes duplicate
stack traces to be replaced with 8-character
+ * hash strings.
+ * </ul>
+ *
+ * <p>
+ * This class converts the format strings into a regular expression that can
be used to parse the resulting log file.
+ */
+public class LogEntryFormatter extends Formatter {
+
+ private ConcurrentHashMap<String,AtomicInteger> hashes;
+ private DateFormat df;
+ private String format;
+ private Pattern rePattern;
+ private Map<String,Integer> fieldIndexes;
+
+ /**
+ * Create a new formatter.
+ *
+ * @param format
+ * The log entry format. e.g. <js>"[{date} {level}] {msg}%n"</js>
+ * The string can contain any of the following variables:
+ * <ol>
+ * <li><js>"{date}"</js> - The date, formatted per
<js>"Logging/dateFormat"</js>.
+ * <li><js>"{class}"</js> - The class name.
+ * <li><js>"{method}"</js> - The method name.
+ * <li><js>"{logger}"</js> - The logger name.
+ * <li><js>"{level}"</js> - The log level name.
+ * <li><js>"{msg}"</js> - The log message.
+ * <li><js>"{threadid}"</js> - The thread ID.
+ * <li><js>"{exception}"</js> - The localized exception
message.
+ * </ol>
+ * @param dateFormat
+ * The {@link SimpleDateFormat} format to use for dates. e.g.
<js>"yyyy.MM.dd hh:mm:ss"</js>.
+ * @param useStackTraceHashes
+ * If <jk>true</jk>, only print unique stack traces once and then
refer to them by a simple 8 character hash
+ * identifier.
+ */
+ public LogEntryFormatter(String format, String dateFormat, boolean
useStackTraceHashes) {
+ this.df = new SimpleDateFormat(dateFormat);
+ if (useStackTraceHashes)
+ hashes = new ConcurrentHashMap<>();
+
+ fieldIndexes = new HashMap<>();
+
+ format = format
+ .replaceAll("\\{date\\}", "%1\\$s")
+ .replaceAll("\\{class\\}", "%2\\$s")
+ .replaceAll("\\{method\\}", "%3\\$s")
+ .replaceAll("\\{logger\\}", "%4\\$s")
+ .replaceAll("\\{level\\}", "%5\\$s")
+ .replaceAll("\\{msg\\}", "%6\\$s")
+ .replaceAll("\\{threadid\\}", "%7\\$s")
+ .replaceAll("\\{exception\\}", "%8\\$s");
+
+ this.format = format;
+
+ // Construct a regular expression to match this log entry.
+ int index = 1;
+ StringBuilder re = new StringBuilder();
+ int S1 = 1; // Looking for %
+ int S2 = 2; // Found %, looking for number.
+ int S3 = 3; // Found number, looking for $.
+ int S4 = 4; // Found $, looking for s.
+ int state = 1;
+ int i1 = 0;
+ for (int i = 0; i < format.length(); i++) {
+ char c = format.charAt(i);
+ if (state == S1) {
+ if (c == '%')
+ state = S2;
+ else {
+ if (! (Character.isLetterOrDigit(c) ||
Character.isWhitespace(c)))
+ re.append('\\');
+ re.append(c);
+ }
+ } else if (state == S2) {
+ if (Character.isDigit(c)) {
+ i1 = i;
+ state = S3;
+ } else {
+ re.append("\\%").append(c);
+ state = S1;
+ }
+ } else if (state == S3) {
+ if (c == '$') {
+ state = S4;
+ } else {
+
re.append("\\%").append(format.substring(i1, i));
+ state = S1;
+ }
+ } else if (state == S4) {
+ if (c == 's') {
+ int group =
Integer.parseInt(format.substring(i1, i-1));
+ switch (group) {
+ case 1:
+
fieldIndexes.put("date", index++);
+ re.append("(" +
dateFormat.replaceAll("[mHhsSdMy]", "\\\\d").replaceAll("\\.", "\\\\.") + ")");
+ break;
+ case 2:
+
fieldIndexes.put("class", index++);
+
re.append("([\\p{javaJavaIdentifierPart}\\.]+)");
+ break;
+ case 3:
+
fieldIndexes.put("method", index++);
+
re.append("([\\p{javaJavaIdentifierPart}\\.]+)");
+ break;
+ case 4:
+
fieldIndexes.put("logger", index++);
+
re.append("([\\w\\d\\.\\_]+)");
+ break;
+ case 5:
+
fieldIndexes.put("level", index++);
+
re.append("(SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST)");
+ break;
+ case 6:
+ fieldIndexes.put("msg",
index++);
+ re.append("(.*)");
+ break;
+ case 7:
+
fieldIndexes.put("threadid", index++);
+ re.append("(\\\\d+)");
+ break;
+ case 8:
+
fieldIndexes.put("exception", index++);
+ re.append("(.*)");
+ break;
+ default: // Fall through.
+ }
+ } else {
+
re.append("\\%").append(format.substring(i1, i));
+ }
+ state = S1;
+ }
+ }
+
+ // The log parser
+ String sre = re.toString();
+ if (sre.endsWith("\\%n"))
+ sre = sre.substring(0, sre.length()-3);
+
+ // Replace instances of %n.
+ sre = sre.replaceAll("\\\\%n", "\\\\n");
+
+ rePattern = Pattern.compile(sre);
+ fieldIndexes = mapFrom(fieldIndexes);
+ }
+
+ /**
+ * Returns the regular expression pattern used for matching log entries.
+ *
+ * @return The regular expression pattern used for matching log entries.
+ */
+ public Pattern getLogEntryPattern() {
+ return rePattern;
+ }
+
+ /**
+ * Returns the {@link DateFormat} used for matching dates.
+ *
+ * @return The {@link DateFormat} used for matching dates.
+ */
+ public DateFormat getDateFormat() {
+ return df;
+ }
+
+ /**
+ * Given a matcher that has matched the pattern specified by {@link
#getLogEntryPattern()}, returns the field value
+ * from the match.
+ *
+ * @param fieldName
+ * The field name.
+ * Possible values are:
+ * <ul>
+ * <li><js>"date"</js>
+ * <li><js>"class"</js>
+ * <li><js>"method"</js>
+ * <li><js>"logger"</js>
+ * <li><js>"level"</js>
+ * <li><js>"msg"</js>
+ * <li><js>"threadid"</js>
+ * <li><js>"exception"</js>
+ * </ul>
+ * @param m The matcher.
+ * @return The field value, or <jk>null</jk> if the specified field
does not exist.
+ */
+ public String getField(String fieldName, Matcher m) {
+ Integer i = fieldIndexes.get(fieldName);
+ return (i == null ? null : m.group(i));
+ }
+
+ @Override /* Formatter */
+ public String format(LogRecord r) {
+ String msg = formatMessage(r);
+ Throwable t = r.getThrown();
+ String hash = null;
+ int c = 0;
+ if (hashes != null && t != null) {
+ hash = hashCode(t);
+ hashes.putIfAbsent(hash, new AtomicInteger(0));
+ c = hashes.get(hash).incrementAndGet();
+ if (c == 1) {
+ msg = '[' + hash + '.' + c + "] " + msg;
+ } else {
+ msg = '[' + hash + '.' + c + "] " + msg + ", "
+ t.getLocalizedMessage();
+ t = null;
+ }
+ }
+ String s = String.format(format,
+ df.format(new Date(r.getMillis())),
+ r.getSourceClassName(),
+ r.getSourceMethodName(),
+ r.getLoggerName(),
+ r.getLevel(),
+ msg,
+ r.getThreadID(),
+ r.getThrown() == null ? "" :
r.getThrown().getMessage());
+ if (t != null)
+ s += String.format("%n%s",
ThrowableUtils.getStackTrace(r.getThrown()));
+ return s;
+ }
+
+ private static String hashCode(Throwable t) {
+ int i = 0;
+ while (t != null) {
+ for (StackTraceElement e : t.getStackTrace())
+ i ^= e.hashCode();
+ t = t.getCause();
+ }
+ return Integer.toHexString(i);
+ }
+}
diff --git
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogParser.java
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogParser.java
new file mode 100644
index 000000000..78b36488e
--- /dev/null
+++
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogParser.java
@@ -0,0 +1,224 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.microservice.resources;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.text.*;
+import java.util.*;
+import java.util.regex.*;
+
+/**
+ * Utility class for reading log files.
+ *
+ * <p>
+ * Provides the capability of returning splices of log files based on dates
and filtering based on thread and logger
+ * names.
+ */
+public final class LogParser implements Iterable<LogParser.Entry>,
Iterator<LogParser.Entry>, Closeable {
+ private BufferedReader br;
+ LogEntryFormatter formatter;
+ Date start, end;
+ Set<String> loggerFilter, severityFilter;
+ String threadFilter;
+ private Entry next;
+
+ /**
+ * Constructor.
+ *
+ * @param formatter The log entry formatter.
+ * @param f The log file.
+ * @param start Don't return rows before this date. If <jk>null</jk>,
start from the beginning of the file.
+ * @param end Don't return rows after this date. If <jk>null</jk>, go
to the end of the file.
+ * @param thread Only return log entries with this thread name.
+ * @param loggers Only return log entries produced by these loggers
(simple class names).
+ * @param severity Only return log entries with the specified severity.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public LogParser(LogEntryFormatter formatter, File f, Date start, Date
end, String thread, String[] loggers, String[] severity) throws IOException {
+ br = new BufferedReader(new InputStreamReader(new
FileInputStream(f), Charset.defaultCharset()));
+ this.formatter = formatter;
+ this.start = start;
+ this.end = end;
+ this.threadFilter = thread;
+ if (loggers != null)
+ this.loggerFilter = new
LinkedHashSet<>(Arrays.asList(loggers));
+ if (severity != null)
+ this.severityFilter = new
LinkedHashSet<>(Arrays.asList(severity));
+
+ // Find the first line.
+ String line;
+ while (next == null && (line = br.readLine()) != null) {
+ Entry e = new Entry(line);
+ if (e.matches())
+ next = e;
+ }
+ }
+
+ @Override /* Iterator */
+ public boolean hasNext() {
+ return next != null;
+ }
+
+ @Override /* Iterator */
+ public Entry next() {
+ Entry current = next;
+ Entry prev = next;
+ try {
+ next = null;
+ String line = null;
+ while (next == null && (line = br.readLine()) != null) {
+ Entry e = new Entry(line);
+ if (e.isRecord) {
+ if (e.matches())
+ next = e;
+ prev = null;
+ } else {
+ if (prev != null)
+ prev.addText(e.line);
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return current;
+ }
+
+ @Override /* Iterator */
+ public void remove() {
+ throw new NoSuchMethodError();
+ }
+
+ @Override /* Iterable */
+ public Iterator<Entry> iterator() {
+ return this;
+ }
+
+ @Override /* Closeable */
+ public void close() throws IOException {
+ br.close();
+ }
+
+ /**
+ * Serializes the contents of the parsed log file to the specified
writer and then closes the underlying reader.
+ *
+ * @param w The writer to write the log file to.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public void writeTo(Writer w) throws IOException {
+ try {
+ if (! hasNext())
+ w.append("[EMPTY]");
+ else for (LogParser.Entry le : this)
+ le.append(w);
+ } finally {
+ close();
+ }
+ }
+
+ /**
+ * Represents a single line from the log file.
+ */
+ @SuppressWarnings("javadoc")
+ public final class Entry {
+ public Date date;
+ public String severity, logger;
+ protected String line, text;
+ protected String thread;
+ protected List<String> additionalText;
+ protected boolean isRecord;
+
+ Entry(String line) throws IOException {
+ try {
+ this.line = line;
+ Matcher m =
formatter.getLogEntryPattern().matcher(line);
+ if (m.matches()) {
+ isRecord = true;
+ String s = formatter.getField("date",
m);
+ if (s != null)
+ date =
formatter.getDateFormat().parse(s);
+ thread = formatter.getField("thread",
m);
+ severity = formatter.getField("level",
m);
+ logger = formatter.getField("logger",
m);
+ text = formatter.getField("msg", m);
+ if (logger != null &&
logger.indexOf('.') > -1)
+ logger =
logger.substring(logger.lastIndexOf('.')+1);
+ }
+ } catch (ParseException e) {
+ throw new IOException(e);
+ }
+ }
+
+ void addText(String t) {
+ if (additionalText == null)
+ additionalText = new LinkedList<>();
+ additionalText.add(t);
+ }
+
+ public String getText() {
+ if (additionalText == null)
+ return text;
+ int i = text.length();
+ for (String s : additionalText)
+ i += s.length() + 1;
+ StringBuilder sb = new StringBuilder(i);
+ sb.append(text);
+ for (String s : additionalText)
+ sb.append('\n').append(s);
+ return sb.toString();
+ }
+
+ public String getThread() {
+ return thread;
+ }
+
+ public Writer appendHtml(Writer w) throws IOException {
+ w.append(toHtml(line)).append("<br>");
+ if (additionalText != null)
+ for (String t : additionalText)
+ w.append(toHtml(t)).append("<br>");
+ return w;
+ }
+
+ protected Writer append(Writer w) throws IOException {
+ w.append(line).append('\n');
+ if (additionalText != null)
+ for (String t : additionalText)
+ w.append(t).append('\n');
+ return w;
+ }
+
+ boolean matches() {
+ if (! isRecord)
+ return false;
+ if (start != null && date.before(start))
+ return false;
+ if (end != null && date.after(end))
+ return false;
+ if (threadFilter != null && !
threadFilter.equals(thread))
+ return false;
+ if (loggerFilter != null && !
loggerFilter.contains(logger))
+ return false;
+ if (severityFilter != null && !
severityFilter.contains(severity))
+ return false;
+ return true;
+ }
+ }
+
+ static final String toHtml(String s) {
+ if (s.indexOf('<') != -1)
+ return s.replaceAll("<", "<");//$NON-NLS-2$
+ return s;
+ }
+}
+
diff --git
a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogsResource.java
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogsResource.java
new file mode 100644
index 000000000..07bd4f8bc
--- /dev/null
+++
b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/resources/LogsResource.java
@@ -0,0 +1,338 @@
+//
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file *
+// * distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance *
+// * with the License. You may obtain a copy of the License at
*
+// *
*
+// * http://www.apache.org/licenses/LICENSE-2.0
*
+// *
*
+// * Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
express or implied. See the License for the *
+// * specific language governing permissions and limitations under the
License. *
+//
***************************************************************************************************************************
+package org.apache.juneau.microservice.resources;
+
+import static org.apache.juneau.rest.annotation.HookEvent.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.config.*;
+import org.apache.juneau.dto.*;
+import org.apache.juneau.html.annotation.*;
+import org.apache.juneau.http.annotation.Path;
+import org.apache.juneau.http.annotation.Query;
+import org.apache.juneau.http.annotation.Response;
+import org.apache.juneau.http.response.*;
+import org.apache.juneau.rest.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.beans.*;
+import org.apache.juneau.rest.config.*;
+import org.apache.juneau.rest.converter.*;
+import org.apache.juneau.rest.servlet.*;
+
+/**
+ * REST resource for viewing and accessing log files.
+ */
+@Rest(
+ path="/logs",
+ title="Log files",
+ description="Log files from this service",
+ allowedMethodParams="*"
+)
+@HtmlConfig(uriAnchorText="PROPERTY_NAME")
+@SuppressWarnings("javadoc")
+public class LogsResource extends BasicRestServlet implements
BasicUniversalConfig {
+ private static final long serialVersionUID = 1L;
+
+
//-------------------------------------------------------------------------------------------------------------------
+ // Instance
+
//-------------------------------------------------------------------------------------------------------------------
+
+ private File logDir;
+ private LogEntryFormatter leFormatter;
+ boolean allowDeletes;
+
+ @RestHook(INIT)
+ public void init(Config config) throws Exception {
+ logDir = new
File(config.get("Logging/logDir").asString().orElse("logs"));
+ allowDeletes =
config.get("Logging/allowDeletes").asBoolean().orElse(true);
+ leFormatter = new LogEntryFormatter(
+ config.get("Logging/format").asString().orElse("[{date}
{level}] {msg}%n"),
+
config.get("Logging/dateFormat").asString().orElse("yyyy.MM.dd hh:mm:ss"),
+
config.get("Logging/useStackTraceHashes").asBoolean().orElse(true)
+ );
+ }
+
+ @RestGet(
+ path="/*",
+ summary="View information on file or directory",
+ description="Returns information about the specified file or
directory."
+ )
+ @HtmlDocConfig(
+ nav={"<h5>Folder: $RA{fullPath}</h5>"}
+ )
+ public FileResource getFile(RestRequest req, @Path("/*") String path)
throws NotFound, Exception {
+
+ File dir = getFile(path);
+ req.setAttribute("fullPath", dir.getAbsolutePath());
+
+ return new FileResource(dir, path, allowDeletes, true);
+ }
+
+ @RestOp(
+ method="VIEW",
+ path="/*",
+ summary="View contents of log file",
+ description="View the contents of a log file."
+ )
+ public void viewFile(
+ RestResponse res,
+ @Path("/*") String path,
+ @Query(name="highlight", schema=@Schema(d="Add severity
color highlighting.")) boolean highlight,
+ @Query(name="start", schema=@Schema(d="Start timestamp
(ISO8601, full or partial).\nDon't print lines logged before the specified
timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd,
yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss,
yyyy-MM-ddThh:mm:ss.SSS")) String start,
+ @Query(name="end", schema=@Schema(d="End timestamp
(ISO8601, full or partial).\nDon't print lines logged after the specified
timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd,
yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss,
yyyy-MM-ddThh:mm:ss.SSS")) String end,
+ @Query(name="thread", schema=@Schema(d="Thread name
filter.\nOnly show log entries with the specified thread name.")) String thread,
+ @Query(name="loggers", schema=@Schema(d="Logger filter
(simple class name).\nOnly show log entries if they were produced by one of the
specified loggers.")) String[] loggers,
+ @Query(name="severity",schema=@Schema( d="Severity
filter.\nOnly show log entries with the specified severity.")) String[] severity
+ ) throws NotFound, MethodNotAllowed, IOException {
+
+ File f = getFile(path);
+
+ Date startDate = parseIsoDate(start), endDate =
parseIsoDate(end);
+
+ if (! highlight) {
+ Object o = getReader(f, startDate, endDate, thread,
loggers, severity);
+ res.setContentType("text/plain");
+ if (o instanceof Reader)
+ res.setContent(o);
+ else {
+ try (LogParser p = (LogParser)o; Writer w =
res.getNegotiatedWriter()) {
+ p.writeTo(w);
+ }
+ }
+ return;
+ }
+
+ res.setContentType("text/html");
+ try (PrintWriter w = res.getNegotiatedWriter()) {
+ w.println("<html><body
style='font-family:monospace;font-size:8pt;white-space:pre;'>");
+ try (LogParser lp = getLogParser(f, startDate, endDate,
thread, loggers, severity)) {
+ if (! lp.hasNext())
+ w.append("<span
style='color:gray'>[EMPTY]</span>");
+ else for (LogParser.Entry le : lp) {
+ char s = le.severity.charAt(0);
+ String color = "black";
+
//SEVERE|WARNING|INFO|CONFIG|FINE|FINER|FINEST
+ if (s == 'I')
+ color = "#006400";
+ else if (s == 'W')
+ color = "#CC8400";
+ else if (s == 'E' || s == 'S')
+ color = "#DD0000";
+ else if (s == 'D' || s == 'F' || s ==
'T')
+ color = "#000064";
+ w.append("<span
style='color:").append(color).append("'>");
+ le.appendHtml(w).append("</span>");
+ }
+ w.append("</body></html>");
+ }
+ }
+ }
+
+ @RestOp(
+ method="PARSE",
+ path="/*",
+ converters=Queryable.class,
+ summary="View parsed contents of file",
+ description="View the parsed contents of a file.",
+ swagger=@OpSwagger(
+ parameters={
+ Queryable.SWAGGER_PARAMS
+ }
+ )
+ )
+ @HtmlDocConfig(
+ nav={"<h5>Folder: $RA{fullPath}</h5>"}
+ )
+ public LogParser viewParsedEntries(
+ RestRequest req,
+ @Path("/*") String path,
+ @Query(name="start", schema=@Schema(d="Start timestamp
(ISO8601, full or partial).\nDon't print lines logged before the specified
timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd,
yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss,
yyyy-MM-ddThh:mm:ss.SSS")) String start,
+ @Query(name="end", schema=@Schema(d="End timestamp
(ISO8601, full or partial).\nDon't print lines logged after the specified
timestamp.\nUse any of the following formats: yyyy, yyyy-MM, yyyy-MM-dd,
yyyy-MM-ddThh, yyyy-MM-ddThh:mm, yyyy-MM-ddThh:mm:ss,
yyyy-MM-ddThh:mm:ss.SSS")) String end,
+ @Query(name="thread", schema=@Schema(d="Thread name
filter.\nOnly show log entries with the specified thread name.")) String thread,
+ @Query(name="loggers", schema=@Schema(d="Logger filter
(simple class name).\nOnly show log entries if they were produced by one of the
specified loggers.")) String[] loggers,
+ @Query(name="severity", schema=@Schema(d="Severity
filter.\nOnly show log entries with the specified severity.")) String[] severity
+ ) throws NotFound, IOException {
+
+ File f = getFile(path);
+ req.setAttribute("fullPath", f.getAbsolutePath());
+
+ Date startDate = parseIsoDate(start), endDate =
parseIsoDate(end);
+
+ return getLogParser(f, startDate, endDate, thread, loggers,
severity);
+ }
+
+ @RestOp(
+ method="DOWNLOAD",
+ path="/*",
+ summary="Download file",
+ description="Download the contents of a file.\nContent-Type is
set to 'application/octet-stream'."
+ )
+ public FileContents downloadFile(RestResponse res, @Path("/*") String
path) throws NotFound, MethodNotAllowed {
+ res.setContentType("application/octet-stream");
+ try {
+ return new FileContents(getFile(path));
+ } catch (FileNotFoundException e) {
+ throw new NotFound("File not found");
+ }
+ }
+
+ @RestDelete(
+ path="/*",
+ summary="Delete log file",
+ description="Delete a log file on the file system."
+ )
+ public RedirectToRoot deleteFile(@Path("/*") String path) throws
MethodNotAllowed {
+ deleteFile(getFile(path));
+ return new RedirectToRoot();
+ }
+
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Helper beans
+
//-----------------------------------------------------------------------------------------------------------------
+
+
@Response(schema=@Schema(type="string",format="binary",description="Contents of
file"))
+ static class FileContents extends FileInputStream {
+ public FileContents(File file) throws FileNotFoundException {
+ super(file);
+ }
+ }
+
+ @Response(schema=@Schema(description="Redirect to root page on
success"))
+ static class RedirectToRoot extends SeeOtherRoot {}
+
+ @Response(schema=@Schema(description="File action"))
+ public static class Action extends LinkString {
+ public Action(String name, String uri, Object...uriArgs) {
+ super(name, uri, uriArgs);
+ }
+ }
+
+ @Response(schema=@Schema(description="File or directory details"))
+ @Bean(properties="type,name,size,lastModified,actions,files")
+ public static class FileResource {
+ private final File f;
+ private final String path;
+ private final String uri;
+ private final boolean includeChildren, allowDeletes;
+
+ public FileResource(File f, String path, boolean allowDeletes,
boolean includeChildren) {
+ this.f = f;
+ this.path = path;
+ this.uri = "servlet:/"+(path == null ? "" : path);
+ this.includeChildren = includeChildren;
+ this.allowDeletes = allowDeletes;
+ }
+
+ public String getType() {
+ return (f.isDirectory() ? "dir" : "file");
+ }
+
+ public LinkString getName() {
+ return new LinkString(f.getName(), uri);
+ }
+
+ public long getSize() {
+ return f.isDirectory() ? f.listFiles().length :
f.length();
+ }
+
+ public Date getLastModified() {
+ return new Date(f.lastModified());
+ }
+
+ @Html(format=HtmlFormat.HTML_CDC)
+ public List<Action> getActions() throws Exception {
+ List<Action> l = new ArrayList<>();
+ if (f.canRead() && ! f.isDirectory()) {
+ l.add(new Action("view", uri + "?method=VIEW"));
+ l.add(new Action("highlighted", uri +
"?method=VIEW&highlight=true"));
+ l.add(new Action("parsed", uri +
"?method=PARSE"));
+ l.add(new Action("download", uri +
"?method=DOWNLOAD"));
+ if (allowDeletes)
+ l.add(new Action("delete", uri +
"?method=DELETE"));
+ }
+ return l;
+ }
+
+ public Set<FileResource> getFiles() {
+ if (f.isFile() || ! includeChildren)
+ return null;
+ Set<FileResource> s = new TreeSet<>(FILE_COMPARATOR);
+ for (File fc : f.listFiles(FILE_FILTER))
+ s.add(new FileResource(fc, (path != null ?
(path + '/') : "") + urlEncode(fc.getName()), allowDeletes, false));
+ return s;
+ }
+
+ static final FileFilter FILE_FILTER = new FileFilter() {
+ @Override /* FileFilter */
+ public boolean accept(File f) {
+ return f.isDirectory() ||
f.getName().endsWith(".log");
+ }
+ };
+
+ static final Comparator<FileResource> FILE_COMPARATOR = new
Comparator<FileResource>() {
+ @Override /* Comparator */
+ public int compare(FileResource o1, FileResource o2) {
+ int c = o1.getType().compareTo(o2.getType());
+ return c != 0 ? c :
o1.getName().compareTo(o2.getName());
+ }
+ };
+ }
+
+
+
//-----------------------------------------------------------------------------------------------------------------
+ // Helper methods
+
//-----------------------------------------------------------------------------------------------------------------
+
+ private File getFile(String path) throws NotFound {
+ if (path == null)
+ return logDir;
+ File f = new File(logDir.getAbsolutePath() + '/' + path);
+ if (f.exists())
+ return f;
+ throw new NotFound("File not found.");
+ }
+
+ private void deleteFile(File f) {
+ if (! allowDeletes)
+ throw new MethodNotAllowed("DELETE not enabled");
+ if (f.isDirectory()) {
+ File[] files = f.listFiles();
+ if (files != null) {
+ for (File fc : files)
+ deleteFile(fc);
+ }
+ }
+ if (! f.delete())
+ throw new Forbidden("Could not delete file {0}",
f.getAbsolutePath()) ;
+ }
+
+ private static BufferedReader getReader(File f) throws IOException {
+ return new BufferedReader(new InputStreamReader(new
FileInputStream(f), Charset.defaultCharset()));
+ }
+
+ private Object getReader(File f, final Date start, final Date end,
final String thread, final String[] loggers, final String[] severity) throws
IOException {
+ if (start == null && end == null && thread == null && loggers
== null)
+ return getReader(f);
+ return getLogParser(f, start, end, thread, loggers, severity);
+ }
+
+ private LogParser getLogParser(File f, final Date start, final Date
end, final String thread, final String[] loggers, final String[] severity)
throws IOException {
+ return new LogParser(leFormatter, f, start, end, thread,
loggers, severity);
+ }
+}