This is an automated email from the ASF dual-hosted git repository. jbonofre pushed a commit to branch karaf-4.3.x in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/karaf-4.3.x by this push: new 6d10adbf4b [KARAF-7423] Add optional logback configuration 6d10adbf4b is described below commit 6d10adbf4bcc767443586c26d194cbdedf1c4b6e Author: Ina Thiemann <ina.thiem...@kisters.de> AuthorDate: Thu Apr 14 16:04:38 2022 +0200 [KARAF-7423] Add optional logback configuration (cherry picked from commit 9b6c7775f30c84474d2a4fe0e8bd2f9624e1f90d) --- assemblies/features/framework/pom.xml | 4 + log/pom.xml | 8 +- .../karaf/log/core/internal/LogServiceImpl.java | 8 + .../core/internal/LogServiceLogbackXmlImpl.java | 266 +++++++++++++++++++++ .../core/internal/LogServiceLogbackXmlTest.java | 104 ++++++++ manual/src/main/asciidoc/user-guide/log.adoc | 27 +++ 6 files changed, 416 insertions(+), 1 deletion(-) diff --git a/assemblies/features/framework/pom.xml b/assemblies/features/framework/pom.xml index 779c1462b1..9117422b65 100644 --- a/assemblies/features/framework/pom.xml +++ b/assemblies/features/framework/pom.xml @@ -102,6 +102,10 @@ <groupId>org.ops4j.pax.logging</groupId> <artifactId>pax-logging-log4j2</artifactId> </dependency> + <dependency> + <groupId>org.ops4j.pax.logging</groupId> + <artifactId>pax-logging-logback</artifactId> + </dependency> <dependency> <groupId>org.ops4j.pax.url</groupId> <artifactId>pax-url-aether</artifactId> diff --git a/log/pom.xml b/log/pom.xml index 11a55bc5b2..3f24caabc8 100644 --- a/log/pom.xml +++ b/log/pom.xml @@ -32,7 +32,7 @@ <artifactId>org.apache.karaf.log.core</artifactId> <packaging>bundle</packaging> <name>Apache Karaf :: Log :: Core</name> - <description>Core Seervices and JMX MBean to manipulate the Karaf log layer</description> + <description>Core Services and JMX MBean to manipulate the Karaf log layer</description> <properties> <appendedResourcesDirectory>${basedir}/../../../etc/appended-resources/</appendedResourcesDirectory> @@ -67,6 +67,12 @@ <artifactId>pax-logging-log4j2</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.ops4j.pax.logging</groupId> + <artifactId>pax-logging-logback</artifactId> + <version>${pax.logging.version}</version> + <scope>provided</scope> + </dependency> <dependency> <groupId>org.ops4j.pax.logging</groupId> <artifactId>pax-logging-api</artifactId> diff --git a/log/src/main/java/org/apache/karaf/log/core/internal/LogServiceImpl.java b/log/src/main/java/org/apache/karaf/log/core/internal/LogServiceImpl.java index f291ac5951..bf0eb76203 100644 --- a/log/src/main/java/org/apache/karaf/log/core/internal/LogServiceImpl.java +++ b/log/src/main/java/org/apache/karaf/log/core/internal/LogServiceImpl.java @@ -59,6 +59,14 @@ public class LogServiceImpl implements LogService, PaxAppender { throw new IllegalStateException("Unsupported Log4j2 configuration type: " + file); } } + else if (config.get("org.ops4j.pax.logging.logback.config.file") != null) { + String file = config.get("org.ops4j.pax.logging.logback.config.file").toString(); + if (file.endsWith(".xml")) { + return new LogServiceLogbackXmlImpl(file); + } else { + throw new IllegalStateException("Unsupported Logback configuration type: " + file); + } + } else { throw new IllegalStateException("Unrecognized configuration"); } diff --git a/log/src/main/java/org/apache/karaf/log/core/internal/LogServiceLogbackXmlImpl.java b/log/src/main/java/org/apache/karaf/log/core/internal/LogServiceLogbackXmlImpl.java new file mode 100644 index 0000000000..ad4924aad2 --- /dev/null +++ b/log/src/main/java/org/apache/karaf/log/core/internal/LogServiceLogbackXmlImpl.java @@ -0,0 +1,266 @@ +/* + * 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.karaf.log.core.internal; + +import org.apache.karaf.log.core.Level; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +public class LogServiceLogbackXmlImpl implements LogServiceInternal { + + private static final String ELEMENT_ROOT = "root"; + private static final String ELEMENT_LOGGER = "logger"; + private static final String ATTRIBUTE_NAME = "name"; + private static final String ATTRIBUTE_LEVEL = "level"; + private static final String ELEMENT_CONFIGURATION = "configuration"; + + private final Path path; + + LogServiceLogbackXmlImpl(String file) { + this.path = Paths.get(file); + } + + public Map<String, String> getLevel(String logger) { + try { + Document doc = loadConfig(path); + Map<String, Element> loggers = getLoggers(doc); + + Map<String, String> levels = new TreeMap<>(); + for (Map.Entry<String, Element> e : loggers.entrySet()) { + String level = e.getValue().getAttribute(ATTRIBUTE_LEVEL); + if (level != null && !level.isEmpty()) { + levels.put(e.getKey(), level); + } + } + + if (ALL_LOGGER.equals(logger)) { + return levels; + } + String l = logger; + String val; + for (; ; ) { + val = levels.get(l != null ? l : ROOT_LOGGER); + if (val != null || l == null) { + return Collections.singletonMap(logger, val); + } + int idx = l.lastIndexOf('.'); + if (idx < 0) { + l = null; + } else { + l = l.substring(0, idx); + } + } + } catch (Exception e) { + throw new RuntimeException("Unable to retrieve level for logger", e); + } + } + + public void setLevel(String logger, String level) { + try { + Document doc = loadConfig(path); + Map<String, Element> loggers = getLoggers(doc); + + Element element = loggers.get(logger); + if (element != null) { + if (Level.isDefault(level)) { + element.removeAttribute(ATTRIBUTE_LEVEL); + } else { + element.setAttribute(ATTRIBUTE_LEVEL, level); + } + } + else if (!Level.isDefault(level)) { + Element docE = doc.getDocumentElement(); + boolean root = ROOT_LOGGER.equals(logger); + if (root) { + element = doc.createElement(ELEMENT_ROOT); + element.setAttribute(ATTRIBUTE_LEVEL, level); + } else { + element = doc.createElement(ELEMENT_LOGGER); + element.setAttribute(ATTRIBUTE_NAME, logger); + element.setAttribute(ATTRIBUTE_LEVEL, level); + } + insertIndented(docE, element); + } else { + return; + } + try (OutputStream os = Files.newOutputStream(path, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { + TransformerFactory tFactory = TransformerFactory.newInstance(); + tFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE); + try { + tFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + tFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + } catch (IllegalArgumentException e) { + // ignore + } + + Transformer transformer = tFactory.newTransformer(); + transformer.transform(new DOMSource(doc), new StreamResult(os)); + } + } catch (Exception e) { + throw new RuntimeException("Unable to set level for logger", e); + } + } + + /** + * Insert the given node into the parent element, + * indenting it as needed. + */ + static void insertIndented(Element parent, Element element) { + NodeList taggedElements = parent.getElementsByTagName("*"); + //only use direct descendants of parent element to insert next to + ArrayList <Node> childElements = new ArrayList<Node>(); + for (int i = 0;i < taggedElements.getLength(); i++ ){ + if(taggedElements.item(i).getParentNode().equals(parent)){ + childElements.add(taggedElements.item(i)); + } + } + Node insertAfter = childElements.size() > 0 ? childElements.get(childElements.size() - 1) : null; + if (insertAfter != null) { + if (insertAfter.getPreviousSibling() != null && insertAfter.getPreviousSibling().getNodeType() == Node.TEXT_NODE) { + String indent = insertAfter.getPreviousSibling().getTextContent(); + Node node = parent.getOwnerDocument().createTextNode(indent); + if (insertAfter.getNextSibling() != null) { + parent.insertBefore(node, insertAfter.getNextSibling()); + insertAfter = node; + } else { + parent.appendChild(node); + } + } + if (insertAfter.getNextSibling() != null ) { + parent.insertBefore(element, insertAfter.getNextSibling()); + } else { + parent.appendChild(element); + } + } else { + String indent; + String prev; + if (parent.getPreviousSibling() != null && parent.getPreviousSibling().getNodeType() == Node.TEXT_NODE) { + indent = parent.getPreviousSibling().getTextContent(); + prev = indent; + if (indent.endsWith("\t")) { + indent += "\t"; + } else { + int nl = indent.lastIndexOf('\n'); + if (nl >= 0) { + indent = indent + indent.substring(nl + 1); + } else { + indent += "\t"; + } + } + if (parent.getFirstChild() != null && parent.getPreviousSibling().getNodeType() == Node.TEXT_NODE) { + parent.removeChild(parent.getFirstChild()); + } + } else { + indent = "\t"; + prev = "\n"; + } + parent.appendChild(parent.getOwnerDocument().createTextNode(indent)); + parent.appendChild(element); + parent.appendChild(parent.getOwnerDocument().createTextNode(prev)); + } + } + + static Document loadConfig(Path path) throws Exception { + try (InputStream is = Files.newInputStream(path)) { + return loadConfig(path.toString(), is); + } + } + + static Document loadConfig(String id, InputStream is) throws ParserConfigurationException, SAXException, IOException { + final InputSource source = new InputSource(is); + source.setPublicId(id); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + factory.setExpandEntityReferences(false); + + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + setFeature(factory, "http://xml.org/sax/features/external-general-entities", false); + setFeature(factory, "http://xml.org/sax/features/external-parameter-entities", false); + setFeature(factory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + setFeature(factory, "http://apache.org/xml/features/xinclude/fixup-base-uris", true); + setFeature(factory, "http://apache.org/xml/features/xinclude/fixup-language", true); + tryCall(() -> factory.setXIncludeAware(true)); + DocumentBuilder documentBuilder = factory.newDocumentBuilder(); + return documentBuilder.parse(source); + } + + private static void setFeature(DocumentBuilderFactory factory, String name, boolean b) { + tryCall(() -> factory.setFeature(name, b)); + } + + interface RunnableWithException { + void run() throws Exception; + } + + private static void tryCall(RunnableWithException c) { + try { + c.run(); + } catch (Exception e) { + // Ignore + } + } + + private Map<String, Element> getLoggers(Document doc) { + Map<String, Element> loggers = new TreeMap<>(); + Element docE = doc.getDocumentElement(); + if (!ELEMENT_CONFIGURATION.equals(docE.getLocalName())) { + throw new IllegalArgumentException("Xml root document should be " + ELEMENT_CONFIGURATION); + } + NodeList loggersList = docE.getElementsByTagName(ELEMENT_LOGGER); + for (int i = 0; i < loggersList.getLength(); i++) { + Node n = loggersList.item(i); + if (n instanceof Element) { + Element e = (Element) n; + if (ELEMENT_ROOT.equals(e.getLocalName())) { + loggers.put(ROOT_LOGGER, e); + } else if (ELEMENT_LOGGER.equals(e.getLocalName())) { + String name = e.getAttribute(ATTRIBUTE_NAME); + if (name != null) { + loggers.put(name, e); + } + } + } + } + return loggers; + } + +} diff --git a/log/src/test/java/org/apache/karaf/log/core/internal/LogServiceLogbackXmlTest.java b/log/src/test/java/org/apache/karaf/log/core/internal/LogServiceLogbackXmlTest.java new file mode 100644 index 0000000000..70b62571a6 --- /dev/null +++ b/log/src/test/java/org/apache/karaf/log/core/internal/LogServiceLogbackXmlTest.java @@ -0,0 +1,104 @@ +/* + * 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.karaf.log.core.internal; + +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.ByteArrayInputStream; +import java.io.StringWriter; + +import static org.junit.Assert.assertEquals; + +public class LogServiceLogbackXmlTest { + + @Test + public void testInsertIndentedTabs() throws Exception { + String xml = "<configuration>\n" + + "</configuration>"; + + String out = insertIndented(xml); + assertEquals( + "<configuration>\n" + + "\t<logger/>\n" + + "</configuration>", out); + } + + @Test + public void testInsertIndentedSpaces() throws Exception { + //this one tests with one logger already added, because with no loggers there is no indentation to decide by and the function will choose tab + String xml = "<configuration>\n" + + " <logger/>\n" + + "</configuration>"; + + String out = insertIndented(xml); + assertEquals( + "<configuration>\n" + + " <logger/>\n" + + " <logger/>\n" + + "</configuration>", out); + } + + @Test + public void testInsertIndentedTabsWithRoot() throws Exception { + String xml = "<configuration>\n" + + "\t<root/>\n" + + "</configuration>"; + + String out = insertIndented(xml); + assertEquals( + "<configuration>\n" + + "\t<root/>\n" + + "\t<logger/>\n" + + "</configuration>", out); + } + + @Test + public void testInsertIndentedSpacesWithRoot() throws Exception { + String xml = "<configuration>\n" + + " <root/>\n" + + "</configuration>"; + + String out = insertIndented(xml); + assertEquals( + "<configuration>\n" + + " <root/>\n" + + " <logger/>\n" + + "</configuration>", out); + } + + private String insertIndented(String xml) throws Exception { + Document doc = LogServiceLog4j2XmlImpl.loadConfig(null, new ByteArrayInputStream(xml.getBytes())); + Element element = doc.createElement("logger"); + LogServiceLogbackXmlImpl.insertIndented( + (Element) doc.getDocumentElement(), + element); + try (StringWriter os = new StringWriter()) { + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.transform(new DOMSource(doc, "the.xml"), new StreamResult(os)); + return os.toString(); + } + } +} diff --git a/manual/src/main/asciidoc/user-guide/log.adoc b/manual/src/main/asciidoc/user-guide/log.adoc index 21e5da7e8d..7edfb38ead 100644 --- a/manual/src/main/asciidoc/user-guide/log.adoc +++ b/manual/src/main/asciidoc/user-guide/log.adoc @@ -160,6 +160,33 @@ A default configuration in `etc/log4j2.xml` could be: </Configuration> ---- +==== Logback support + +Xml based configurations for logback are supported. +To use logback you have to: +. Edit `etc/startup.properties` to replace the line `org/ops4j/pax/logging/pax-logging-service/1.8.4/pax-logging-service-1.8.4.jar=8` with `org/ops4j/pax/logging/pax-logging-service/pax-logging-logback/2.0.14 = 8` +. Add pax-logging-logback jar file in `system/org/ops4j/pax/logging/pax-logging-logback/x.x.x/pax-logging-logback-x.x.x.jar where x.x.x is the version as defined in `etc/startup.properties` +. Alternatively to steps 1 and 2, if using maven with karaf as a dependency, you can add configuration for including logback in the pom.xml like so: +---- + <dependency> + <groupId>org.apache.karaf.features</groupId> + <version>${karaf.version}</version> + <configuration> + <framework>framework-logback</framework> + </configuration> + </dependency> +---- +. Edit `etc/org.ops4j.pax.logging.cfg` configuration file and replace log4j configuration with `org.ops4j.pax.logging.logback.config.file = ${karaf.etc}/logback.xml` +. Add the `etc/logback.xml` configuration file. You need to add a PaxAppender to your configuartion and include it in root. Add the following to logback.xml: +---- + <appender class="org.ops4j.pax.logging.logback.internal.bridges.PaxAppenderDelegate" name="VmLogAppender"/> + <root level="INFO"> + <appender-ref ref="VmLogAppender"/> + {...} + </root> +---- + + ==== `karaf.log.console` property Before Karaf starts _proper_ logging facilities (pax-logging), it may configure `java.util.logging`. Standard