NIFI-5146 Only support HTTP or HTTPS operation for NiFi API/UI

- Added logic to check for simultaneous configuration of HTTP and HTTPS connectors in JettyServer.
- Added test logging resources. Added unit tests.
- Refactored shared functionality to generic method which accepts lambdas.
  Fixed unit test with logging side effects.
- Added note about exclusive HTTP/HTTPS behavior to Admin Guide. Fixed typos.

This closes #2683.

Signed-off-by: Kevin Doran <kdoran@apache.org>
This commit is contained in:
Andy LoPresto 2018-05-03 16:02:19 -07:00 committed by Kevin Doran
parent 92b4a3208f
commit 7a4990e7fe
No known key found for this signature in database
GPG Key ID: 5621A6244B77AC02
5 changed files with 457 additions and 96 deletions

View File

@ -148,7 +148,7 @@ should run on. If it is desired that the HTTPS interface be accessible from all
admins to configure the application to run only on specific network interfaces, `nifi.web.http.network.interface*` or `nifi.web.https.network.interface*`
properties can be specified.
NOTE: It is important when enabling HTTPS that the `nifi.web.http.port` property be unset.
NOTE: It is important when enabling HTTPS that the `nifi.web.http.port` property be unset. NiFi only supports running on HTTP *or* HTTPS, not both simultaneously.
Similar to `nifi.security.needClientAuth`, the web server can be configured to require certificate based client authentication for users accessing
the User Interface. In order to do this it must be configured to not support username/password authentication using <<ldap_login_identity_provider>> or <<kerberos_login_identity_provider>>. Either of these options
@ -1967,8 +1967,8 @@ they must be set the same on every instance in the cluster.
For each Node, the minimum properties to configure are as follows:
* Under the _Web Properties_ section, set either the http or https port that you want the Node to run on.
Also, consider whether you need to set the http or https host property.
* Under the _Web Properties_ section, set either the HTTP or HTTPS port that you want the Node to run on.
Also, consider whether you need to set the HTTP or HTTPS host property. All nodes in the cluster should use the same protocol setting.
* Under the _State Management section_, set the `nifi.state.management.provider.cluster` property
to the identifier of the Cluster State Provider. Ensure that the Cluster State Provider has been
configured in the _state-management.xml_ file. See <<state_providers>> for more information.
@ -2164,7 +2164,7 @@ In order to secure the communications, we need to ensure that both the client an
NiFi ZooKeeper client and embedded ZooKeeper server to use Kerberos are provided below.
If Kerberos is not already setup in your environment, you can find information on installing and setting up a Kerberos Server at
link:https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Managing_Smart_Cards/Configuring_a_Kerberos_5_Server.html[https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Managing_Smart_Cards/Configuring_a_Kerberos_5_Server.html^]. This guide assumes that Kerberos already has been installed in the environment in which NiFi is running.
https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Managing_Smart_Cards/Configuring_a_Kerberos_5_Server.html[Red Hat Customer Portal: Configuring a Kerberos 5 Server]. This guide assumes that Kerberos already has been installed in the environment in which NiFi is running.
Note, the following procedures for kerberizing an Embedded ZooKeeper server in your NiFi Node and kerberizing a ZooKeeper NiFi client will require that
Kerberos client libraries be installed. This is accomplished in Fedora-based Linux distributions via:
@ -2652,7 +2652,7 @@ documentation of the proxy for guidance for your deployment environment and use
** By default, if NiFi is running securely it will only accept HTTP requests with a Host header matching the host[:port] that it is bound to. If NiFi is to accept requests directed to a different
host[:port] the expected values need to be configured. This may be required when running behind a proxy or in a containerized environment. This is configured in a comma
separated list in _nifi.properties_ using the `nifi.web.proxy.host` property (e.g. localhost:18443, proxyhost:443). IPv6 addressed are accepted. Please refer to
separated list in _nifi.properties_ using the `nifi.web.proxy.host` property (e.g. localhost:18443, proxyhost:443). IPv6 addresses are accepted. Please refer to
RFC 5952 Sections link:https://tools.ietf.org/html/rfc5952#section-4[4] and link:https://tools.ietf.org/html/rfc5952#section-6[6] for additional details.
** NiFi will only accept HTTP requests with a X-ProxyContextPath or X-Forwarded-Context header if the value is whitelisted in the `nifi.web.proxy.context.path` property in

View File

@ -22,7 +22,27 @@
</parent>
<artifactId>nifi-jetty</artifactId>
<packaging>jar</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.13</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
@ -168,6 +188,12 @@
<artifactId>nifi-framework-cluster</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-rules</artifactId>
<version>1.16.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -173,6 +173,14 @@ public class JettyServer implements NiFiServer {
server.setHandler(allHandlers);
}
/**
* Instantiates this object but does not perform any configuration. Used for unit testing.
*/
JettyServer(Server server, NiFiProperties properties) {
this.server = server;
this.props = properties;
}
private Handler loadWars(final Set<Bundle> bundles) {
// load WARs
@ -601,106 +609,143 @@ public class JettyServer implements NiFiServer {
httpConfiguration.setRequestHeaderSize(headerSize);
httpConfiguration.setResponseHeaderSize(headerSize);
if (props.getPort() != null) {
final Integer port = props.getPort();
if (port < 0 || (int) Math.pow(2, 16) <= port) {
throw new ServerConfigurationException("Invalid HTTP port: " + port);
}
logger.info("Configuring Jetty for HTTP on port: " + port);
final List<Connector> serverConnectors = Lists.newArrayList();
final Map<String, String> httpNetworkInterfaces = props.getHttpNetworkInterfaces();
if (httpNetworkInterfaces.isEmpty() || httpNetworkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) {
// create the connector
final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
// set host and port
if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.WEB_HTTP_HOST))) {
http.setHost(props.getProperty(NiFiProperties.WEB_HTTP_HOST));
}
http.setPort(port);
serverConnectors.add(http);
} else {
// add connectors for all IPs from http network interfaces
serverConnectors.addAll(Lists.newArrayList(httpNetworkInterfaces.values().stream().map(ifaceName -> {
NetworkInterface iface = null;
try {
iface = NetworkInterface.getByName(ifaceName);
} catch (SocketException e) {
logger.error("Unable to get network interface by name {}", ifaceName, e);
}
if (iface == null) {
logger.warn("Unable to find network interface named {}", ifaceName);
}
return iface;
}).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream())
.map(inetAddress -> {
// create the connector
final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
// set host and port
http.setHost(inetAddress.getHostAddress());
http.setPort(port);
return http;
}).collect(Collectors.toList())));
}
// add all connectors
serverConnectors.forEach(server::addConnector);
// Check if both HTTP and HTTPS connectors are configured and fail if both are configured
if (bothHttpAndHttpsConnectorsConfigured(props)) {
logger.error("NiFi only supports one mode of HTTP or HTTPS operation, not both simultaneously. " +
"Check the nifi.properties file and ensure that either the HTTP hostname and port or the HTTPS hostname and port are empty");
startUpFailure(new IllegalStateException("Only one of the HTTP and HTTPS connectors can be configured at one time"));
}
if (props.getSslPort() != null) {
final Integer port = props.getSslPort();
if (port < 0 || (int) Math.pow(2, 16) <= port) {
throw new ServerConfigurationException("Invalid HTTPs port: " + port);
}
logger.info("Configuring Jetty for HTTPs on port: " + port);
final List<Connector> serverConnectors = Lists.newArrayList();
final Map<String, String> httpsNetworkInterfaces = props.getHttpsNetworkInterfaces();
if (httpsNetworkInterfaces.isEmpty() || httpsNetworkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) {
final ServerConnector https = createUnconfiguredSslServerConnector(server, httpConfiguration);
// set host and port
if (StringUtils.isNotBlank(props.getProperty(NiFiProperties.WEB_HTTPS_HOST))) {
https.setHost(props.getProperty(NiFiProperties.WEB_HTTPS_HOST));
}
https.setPort(port);
serverConnectors.add(https);
} else {
// add connectors for all IPs from https network interfaces
serverConnectors.addAll(Lists.newArrayList(httpsNetworkInterfaces.values().stream().map(ifaceName -> {
NetworkInterface iface = null;
try {
iface = NetworkInterface.getByName(ifaceName);
} catch (SocketException e) {
logger.error("Unable to get network interface by name {}", ifaceName, e);
}
if (iface == null) {
logger.warn("Unable to find network interface named {}", ifaceName);
}
return iface;
}).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream())
.map(inetAddress -> {
final ServerConnector https = createUnconfiguredSslServerConnector(server, httpConfiguration);
// set host and port
https.setHost(inetAddress.getHostAddress());
https.setPort(port);
return https;
}).collect(Collectors.toList())));
}
// add all connectors
serverConnectors.forEach(server::addConnector);
configureHttpsConnector(server, httpConfiguration);
} else if (props.getPort() != null) {
configureHttpConnector(server, httpConfiguration);
} else {
logger.error("Neither the HTTP nor HTTPS connector was configured in nifi.properties");
startUpFailure(new IllegalStateException("Must configure HTTP or HTTPS connector"));
}
}
private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration) {
/**
* Configures an HTTPS connector and adds it to the server.
*
* @param server the Jetty server instance
* @param httpConfiguration the configuration object for the HTTPS protocol settings
*/
private void configureHttpsConnector(Server server, HttpConfiguration httpConfiguration) {
String hostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
final Integer port = props.getSslPort();
String connectorLabel = "HTTPS";
final Map<String, String> httpsNetworkInterfaces = props.getHttpsNetworkInterfaces();
ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> createUnconfiguredSslServerConnector(s, c, port);
configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpsNetworkInterfaces, scc);
}
/**
* Configures an HTTP connector and adds it to the server.
*
* @param server the Jetty server instance
* @param httpConfiguration the configuration object for the HTTP protocol settings
*/
private void configureHttpConnector(Server server, HttpConfiguration httpConfiguration) {
String hostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
final Integer port = props.getPort();
String connectorLabel = "HTTP";
final Map<String, String> httpNetworkInterfaces = props.getHttpNetworkInterfaces();
ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> scc = (s, c) -> new ServerConnector(s, new HttpConnectionFactory(c));
configureGenericConnector(server, httpConfiguration, hostname, port, connectorLabel, httpNetworkInterfaces, scc);
}
/**
* Configures an HTTP(S) connector for the server given the provided parameters. The functionality between HTTP and HTTPS connectors is largely similar.
* Here the common behavior has been extracted into a shared method and the respective calling methods obtain the right values and a lambda function for the differing behavior.
*
* @param server the Jetty server instance
* @param configuration the HTTP/HTTPS configuration instance
* @param hostname the hostname from the nifi.properties file
* @param port the port to expose
* @param connectorLabel used for log output (e.g. "HTTP" or "HTTPS")
* @param networkInterfaces the map of network interfaces from nifi.properties
* @param serverConnectorCreator a function which accepts a {@code Server} and {@code HttpConnection} instance and returns a {@code ServerConnector}
*/
private void configureGenericConnector(Server server, HttpConfiguration configuration, String hostname, Integer port, String connectorLabel, Map<String, String> networkInterfaces,
ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> serverConnectorCreator) {
if (port < 0 || (int) Math.pow(2, 16) <= port) {
throw new ServerConfigurationException("Invalid " + connectorLabel + " port: " + port);
}
logger.info("Configuring Jetty for " + connectorLabel + " on port: " + port);
final List<Connector> serverConnectors = Lists.newArrayList();
// If the interfaces collection is empty or each element is empty
if (networkInterfaces.isEmpty() || networkInterfaces.values().stream().filter(value -> !Strings.isNullOrEmpty(value)).collect(Collectors.toList()).isEmpty()) {
final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
// Set host and port
if (StringUtils.isNotBlank(hostname)) {
serverConnector.setHost(hostname);
}
serverConnector.setPort(port);
serverConnectors.add(serverConnector);
} else {
// Add connectors for all IPs from network interfaces
serverConnectors.addAll(Lists.newArrayList(networkInterfaces.values().stream().map(ifaceName -> {
NetworkInterface iface = null;
try {
iface = NetworkInterface.getByName(ifaceName);
} catch (SocketException e) {
logger.error("Unable to get network interface by name {}", ifaceName, e);
}
if (iface == null) {
logger.warn("Unable to find network interface named {}", ifaceName);
}
return iface;
}).filter(Objects::nonNull).flatMap(iface -> Collections.list(iface.getInetAddresses()).stream())
.map(inetAddress -> {
final ServerConnector serverConnector = serverConnectorCreator.create(server, configuration);
// Set host and port
serverConnector.setHost(inetAddress.getHostAddress());
serverConnector.setPort(port);
return serverConnector;
}).collect(Collectors.toList())));
}
// Add all connectors
serverConnectors.forEach(server::addConnector);
}
/**
* Returns true if there are configured properties for both HTTP and HTTPS connectors (specifically port because the hostname can be left blank in the HTTP connector).
* Prints a warning log message with the relevant properties.
*
* @param props the NiFiProperties
* @return true if both ports are present
*/
static boolean bothHttpAndHttpsConnectorsConfigured(NiFiProperties props) {
Integer httpPort = props.getPort();
String httpHostname = props.getProperty(NiFiProperties.WEB_HTTP_HOST);
Integer httpsPort = props.getSslPort();
String httpsHostname = props.getProperty(NiFiProperties.WEB_HTTPS_HOST);
if (httpPort != null && httpsPort != null) {
logger.warn("Both the HTTP and HTTPS connectors are configured in nifi.properties. Only one of these connectors should be configured. See the NiFi Admin Guide for more details");
logger.warn("HTTP connector: http://" + httpHostname + ":" + httpPort);
logger.warn("HTTPS connector: https://" + httpsHostname + ":" + httpsPort);
return true;
}
return false;
}
private ServerConnector createUnconfiguredSslServerConnector(Server server, HttpConfiguration httpConfiguration, int port) {
// add some secure config
final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
httpsConfiguration.setSecureScheme("https");
httpsConfiguration.setSecurePort(props.getSslPort());
httpsConfiguration.setSecurePort(port);
httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
// build the connector
@ -990,3 +1035,8 @@ public class JettyServer implements NiFiServer {
}
};
}
@FunctionalInterface
interface ServerConnectorCreator<Server, HttpConfiguration, ServerConnector> {
ServerConnector create(Server server, HttpConfiguration httpConfiguration);
}

View File

@ -0,0 +1,258 @@
/*
* 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.nifi.web.server
import org.apache.log4j.AppenderSkeleton
import org.apache.log4j.spi.LoggingEvent
import org.apache.nifi.bundle.Bundle
import org.apache.nifi.properties.StandardNiFiProperties
import org.apache.nifi.util.NiFiProperties
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.eclipse.jetty.server.Connector
import org.eclipse.jetty.server.HttpConfiguration
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.junit.After
import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.contrib.java.lang.system.Assertion
import org.junit.contrib.java.lang.system.ExpectedSystemExit
import org.junit.contrib.java.lang.system.SystemErrRule
import org.junit.contrib.java.lang.system.SystemOutRule
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.security.Security
@RunWith(JUnit4.class)
class JettyServerGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(JettyServerGroovyTest.class)
@Rule
public final ExpectedSystemExit exit = ExpectedSystemExit.none()
@Rule
public final SystemOutRule systemOutRule = new SystemOutRule().enableLog()
@Rule
public final SystemErrRule systemErrRule = new SystemErrRule().enableLog()
@BeforeClass
static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
TestAppender.reset()
}
@AfterClass
static void tearDownOnce() throws Exception {
}
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
TestAppender.reset()
}
@Test
void testShouldDetectHttpAndHttpsConfigurationsBothPresent() {
// Arrange
Map badProps = [
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
(NiFiProperties.WEB_THREADS) : NiFiProperties.DEFAULT_WEB_THREADS
]
NiFiProperties mockProps = [
getPort : { -> 8080 },
getSslPort : { -> 8443 },
getProperty: { String prop ->
String value = badProps[prop] ?: "no_value"
logger.mock("getProperty(${prop}) -> ${value}")
value
},
] as StandardNiFiProperties
// Act
boolean bothConfigsPresent = JettyServer.bothHttpAndHttpsConnectorsConfigured(mockProps)
logger.info("Both configs present: ${bothConfigsPresent}")
def log = TestAppender.getLogLines()
// Assert
assert bothConfigsPresent
assert !log.isEmpty()
assert log.first() =~ "Both the HTTP and HTTPS connectors are configured in nifi.properties. Only one of these connectors should be configured. See the NiFi Admin Guide for more details"
}
@Test
void testDetectHttpAndHttpsConfigurationsShouldAllowEither() {
// Arrange
Map httpMap = [
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
(NiFiProperties.WEB_HTTPS_HOST): null,
]
NiFiProperties httpProps = [
getPort : { -> 8080 },
getSslPort : { -> null },
getProperty: { String prop ->
String value = httpMap[prop] ?: "no_value"
logger.mock("getProperty(${prop}) -> ${value}")
value
},
] as StandardNiFiProperties
Map httpsMap = [
(NiFiProperties.WEB_HTTP_HOST) : null,
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
]
NiFiProperties httpsProps = [
getPort : { -> null },
getSslPort : { -> 8443 },
getProperty: { String prop ->
String value = httpsMap[prop] ?: "no_value"
logger.mock("getProperty(${prop}) -> ${value}")
value
},
] as StandardNiFiProperties
// Act
boolean bothConfigsPresentForHttp = JettyServer.bothHttpAndHttpsConnectorsConfigured(httpProps)
logger.info("Both configs present for HTTP properties: ${bothConfigsPresentForHttp}")
boolean bothConfigsPresentForHttps = JettyServer.bothHttpAndHttpsConnectorsConfigured(httpsProps)
logger.info("Both configs present for HTTPS properties: ${bothConfigsPresentForHttps}")
def log = TestAppender.getLogLines()
// Assert
assert !bothConfigsPresentForHttp
assert !bothConfigsPresentForHttps
// Verifies that the warning was not logged
assert log.size() == 2
assert log.first() == "Both configs present for HTTP properties: false"
assert log.last() == "Both configs present for HTTPS properties: false"
}
@Test
void testShouldFailToStartWithHttpAndHttpsConfigurationsBothPresent() {
// Arrange
Map badProps = [
(NiFiProperties.WEB_HTTP_HOST) : "localhost",
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
]
NiFiProperties mockProps = [
getPort : { -> 8080 },
getSslPort : { -> 8443 },
getProperty: { String prop ->
String value = badProps[prop] ?: "no_value"
logger.mock("getProperty(${prop}) -> ${value}")
value
},
getWebThreads: { -> NiFiProperties.DEFAULT_WEB_THREADS },
getWebMaxHeaderSize: { -> NiFiProperties.DEFAULT_WEB_MAX_HEADER_SIZE },
isHTTPSConfigured: { -> true }
] as StandardNiFiProperties
// The web server should fail to start and exit Java
exit.expectSystemExitWithStatus(1)
exit.checkAssertionAfterwards(new Assertion() {
void checkAssertion() {
final String standardErr = systemErrRule.getLog()
List<String> errLines = standardErr.split("\n")
assert errLines.any { it =~ "Failed to start web server: "}
assert errLines.any { it =~ "Shutting down..."}
}
})
// Act
JettyServer jettyServer = new JettyServer(mockProps, [] as Set<Bundle>)
// Assert
// Assertions defined above
}
@Test
void testShouldConfigureHTTPSConnector() {
// Arrange
NiFiProperties httpsProps = new StandardNiFiProperties(rawProperties: new Properties([
// (NiFiProperties.WEB_HTTP_PORT): null,
// (NiFiProperties.WEB_HTTP_HOST): null,
(NiFiProperties.WEB_HTTPS_PORT): "8443",
(NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
]))
Server internalServer = new Server()
JettyServer jetty = new JettyServer(internalServer, httpsProps)
// Act
jetty.configureHttpsConnector(internalServer, new HttpConfiguration())
List<Connector> connectors = Arrays.asList(internalServer.connectors)
// Assert
assert connectors.size() == 1
ServerConnector connector = connectors.first() as ServerConnector
assert connector.host == "secure.host.com"
assert connector.port == 8443
}
}
class TestAppender extends AppenderSkeleton {
static final List<LoggingEvent> events = new ArrayList<>()
@Override
protected void append(LoggingEvent e) {
synchronized (events) {
events.add(e)
}
}
static void reset() {
synchronized (events) {
events.clear()
}
}
@Override
void close() {
}
@Override
boolean requiresLayout() {
return false
}
static List<String> getLogLines() {
synchronized (events) {
events.collect { LoggingEvent le -> le.getRenderedMessage() }
}
}
}

View File

@ -0,0 +1,27 @@
#
# 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.
#
log4j.rootLogger=DEBUG,console,test
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n
log4j.appender.test=org.apache.nifi.web.server.TestAppender
log4j.appender.test.layout=org.apache.log4j.PatternLayout
log4j.appender.test.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n