LOG4J2-2076 Cassandra appender in own module
Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/d3f9719f Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/d3f9719f Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/d3f9719f Branch: refs/heads/master Commit: d3f9719f9434abb82ec06c7c4eefc06c72d710bf Parents: ff5e664 Author: Mikael StÃ¥ldal <mik...@staldal.nu> Authored: Mon Oct 16 22:01:28 2017 +0200 Committer: Mikael StÃ¥ldal <mik...@staldal.nu> Committed: Mon Oct 16 22:03:33 2017 +0200 ---------------------------------------------------------------------- log4j-cassandra/pom.xml | 200 ++++ .../appender/cassandra/CassandraAppender.java | 185 ++++ .../appender/cassandra/CassandraManager.java | 218 +++++ .../cassandra/ClockTimestampGenerator.java | 34 + .../nosql/appender/cassandra/package-info.java | 23 + log4j-cassandra/src/site/markdown/index.md.vm | 24 + log4j-cassandra/src/site/site.xml | 52 ++ .../appender/cassandra/CassandraAppenderIT.java | 96 ++ .../nosql/appender/cassandra/CassandraRule.java | 141 +++ .../test/resources/CassandraAppenderTest.xml | 40 + .../src/test/resources/cassandra.yaml | 901 +++++++++++++++++++ log4j-nosql/pom.xml | 19 +- .../appender/cassandra/CassandraAppender.java | 185 ---- .../appender/cassandra/CassandraManager.java | 218 ----- .../cassandra/ClockTimestampGenerator.java | 34 - .../nosql/appender/cassandra/package-info.java | 23 - log4j-nosql/src/site/markdown/index.md.vm | 2 - .../appender/cassandra/CassandraAppenderIT.java | 96 -- .../nosql/appender/cassandra/CassandraRule.java | 141 --- .../test/resources/CassandraAppenderTest.xml | 40 - log4j-nosql/src/test/resources/cassandra.yaml | 901 ------------------- pom.xml | 1 + src/site/xdoc/runtime-dependencies.xml | 14 +- 23 files changed, 1929 insertions(+), 1659 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/pom.xml ---------------------------------------------------------------------- diff --git a/log4j-cassandra/pom.xml b/log4j-cassandra/pom.xml new file mode 100644 index 0000000..9fab242 --- /dev/null +++ b/log4j-cassandra/pom.xml @@ -0,0 +1,200 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ 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. + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <parent> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j</artifactId> + <version>2.10.0-SNAPSHOT</version> + </parent> + <modelVersion>4.0.0</modelVersion> + + <artifactId>log4j-cassandra</artifactId> + <name>Apache Log4j Cassandra</name> + <description> + Cassandra appender for Log4j. + </description> + <properties> + <log4jParentDir>${basedir}/..</log4jParentDir> + <docLabel>Cassandra Documentation</docLabel> + <projectDir>/log4j-cassandra</projectDir> + <module.name>org.apache.logging.log4j.cassandra</module.name> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-core</artifactId> + </dependency> + <dependency> + <groupId>com.datastax.cassandra</groupId> + <artifactId>cassandra-driver-core</artifactId> + </dependency> + <!-- Test Dependencies --> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-api</artifactId> + <type>test-jar</type> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-core</artifactId> + <type>test-jar</type> + </dependency> + <!-- Cassandra appender integration testing --> + <dependency> + <groupId>org.apache.cassandra</groupId> + <artifactId>cassandra-all</artifactId> + <version>2.2.8</version> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + </exclusion> + <exclusion> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-core</artifactId> + </exclusion> + </exclusions> + </dependency> + <dependency> + <groupId>org.apache.logging.log4j</groupId> + <artifactId>log4j-slf4j-impl</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <configuration> + <instructions> + <Fragment-Host>org.apache.logging.log4j.core</Fragment-Host> + <Export-Package>*</Export-Package> + </instructions> + </configuration> + </plugin> + </plugins> + </build> + <reporting> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-changes-plugin</artifactId> + <version>${changes.plugin.version}</version> + <reportSets> + <reportSet> + <reports> + <report>changes-report</report> + </reports> + </reportSet> + </reportSets> + <configuration> + <issueLinkTemplate>%URL%/show_bug.cgi?id=%ISSUE%</issueLinkTemplate> + <useJql>true</useJql> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>${checkstyle.plugin.version}</version> + <configuration> + <!--<propertiesLocation>${vfs.parent.dir}/checkstyle.properties</propertiesLocation> --> + <configLocation>${log4jParentDir}/checkstyle.xml</configLocation> + <suppressionsLocation>${log4jParentDir}/checkstyle-suppressions.xml</suppressionsLocation> + <enableRulesSummary>false</enableRulesSummary> + <propertyExpansion>basedir=${basedir}</propertyExpansion> + <propertyExpansion>licensedir=${log4jParentDir}/checkstyle-header.txt</propertyExpansion> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>${javadoc.plugin.version}</version> + <configuration> + <bottom><![CDATA[<p align="center">Copyright © {inceptionYear}-{currentYear} {organizationName}. All Rights Reserved.<br /> + Apache Logging, Apache Log4j, Log4j, Apache, the Apache feather logo, the Apache Logging project logo, + and the Apache Log4j logo are trademarks of The Apache Software Foundation.</p>]]></bottom> + <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating + project --> + <detectOfflineLinks>false</detectOfflineLinks> + <linksource>true</linksource> + </configuration> + <reportSets> + <reportSet> + <id>non-aggregate</id> + <reports> + <report>javadoc</report> + </reports> + </reportSet> + </reportSets> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>findbugs-maven-plugin</artifactId> + <version>${findbugs.plugin.version}</version> + <configuration> + <fork>true</fork> + <jvmArgs>-Duser.language=en</jvmArgs> + <threshold>Normal</threshold> + <effort>Default</effort> + <excludeFilterFile>${log4jParentDir}/findbugs-exclude-filter.xml</excludeFilterFile> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jxr-plugin</artifactId> + <version>${jxr.plugin.version}</version> + <reportSets> + <reportSet> + <id>non-aggregate</id> + <reports> + <report>jxr</report> + </reports> + </reportSet> + <reportSet> + <id>aggregate</id> + <reports> + <report>aggregate</report> + </reports> + </reportSet> + </reportSets> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-pmd-plugin</artifactId> + <version>${pmd.plugin.version}</version> + <configuration> + <targetJdk>${maven.compiler.target}</targetJdk> + </configuration> + </plugin> + </plugins> + </reporting> +</project> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppender.java ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppender.java b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppender.java new file mode 100644 index 0000000..fb344c2 --- /dev/null +++ b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppender.java @@ -0,0 +1,185 @@ +/* + * 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.logging.log4j.nosql.appender.cassandra; + +import com.datastax.driver.core.BatchStatement; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.appender.db.AbstractDatabaseAppender; +import org.apache.logging.log4j.core.appender.db.ColumnMapping; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; +import org.apache.logging.log4j.core.net.SocketAddress; +import org.apache.logging.log4j.core.util.Clock; + +/** + * Appender plugin that uses a Cassandra database. + * + * @see SocketAddress + * @see ColumnMapping + */ +@Plugin(name = "Cassandra", category = Core.CATEGORY_NAME, elementType = CassandraAppender.ELEMENT_TYPE, printObject = true) +public class CassandraAppender extends AbstractDatabaseAppender<CassandraManager> { + + private CassandraAppender(final String name, final Filter filter, final boolean ignoreExceptions, + final CassandraManager manager) { + super(name, filter, ignoreExceptions, manager); + } + + @PluginBuilderFactory + public static <B extends Builder<B>> B newBuilder() { + return new Builder<B>().asBuilder(); + } + + public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B> + implements org.apache.logging.log4j.core.util.Builder<CassandraAppender> { + + /** + * List of Cassandra node contact points. Addresses without a port (or port set to 0) will use the default + * Cassandra port (9042). + */ + @PluginElement("ContactPoints") + @Required(message = "No Cassandra servers provided") + private SocketAddress[] contactPoints = new SocketAddress[]{SocketAddress.getLoopback()}; + + /** + * List of column mappings to convert a LogEvent into a database row. + */ + @PluginElement("Columns") + @Required(message = "No Cassandra columns provided") + private ColumnMapping[] columns; + + @PluginBuilderAttribute + private boolean useTls; + + @PluginBuilderAttribute + @Required(message = "No cluster name provided") + private String clusterName; + + @PluginBuilderAttribute + @Required(message = "No keyspace provided") + private String keyspace; + + @PluginBuilderAttribute + @Required(message = "No table name provided") + private String table; + + @PluginBuilderAttribute + private String username; + + @PluginBuilderAttribute(sensitive = true) + private String password; + + /** + * Override the default TimestampGenerator with one based on the configured {@link Clock}. + */ + @PluginBuilderAttribute + private boolean useClockForTimestampGenerator; + + /** + * Number of LogEvents to buffer before writing. Can be used with or without batch statements. + */ + @PluginBuilderAttribute + private int bufferSize; + + /** + * Whether or not to use batch statements when inserting records. + */ + @PluginBuilderAttribute + private boolean batched; + + /** + * If batch statements are enabled, use this type of batch statement. + */ + @PluginBuilderAttribute + private BatchStatement.Type batchType = BatchStatement.Type.LOGGED; + + public B setContactPoints(final SocketAddress... contactPoints) { + this.contactPoints = contactPoints; + return asBuilder(); + } + + public B setColumns(final ColumnMapping... columns) { + this.columns = columns; + return asBuilder(); + } + + public B setUseTls(final boolean useTls) { + this.useTls = useTls; + return asBuilder(); + } + + public B setClusterName(final String clusterName) { + this.clusterName = clusterName; + return asBuilder(); + } + + public B setKeyspace(final String keyspace) { + this.keyspace = keyspace; + return asBuilder(); + } + + public B setTable(final String table) { + this.table = table; + return asBuilder(); + } + + public B setUsername(final String username) { + this.username = username; + return asBuilder(); + } + + public B setPassword(final String password) { + this.password = password; + return asBuilder(); + } + + public B setUseClockForTimestampGenerator(final boolean useClockForTimestampGenerator) { + this.useClockForTimestampGenerator = useClockForTimestampGenerator; + return asBuilder(); + } + + public B setBufferSize(final int bufferSize) { + this.bufferSize = bufferSize; + return asBuilder(); + } + + public B setBatched(final boolean batched) { + this.batched = batched; + return asBuilder(); + } + + public B setBatchType(final BatchStatement.Type batchType) { + this.batchType = batchType; + return asBuilder(); + } + + @Override + public CassandraAppender build() { + final CassandraManager manager = CassandraManager.getManager(getName(), contactPoints, columns, useTls, + clusterName, keyspace, table, username, password, useClockForTimestampGenerator, bufferSize, batched, + batchType); + return new CassandraAppender(getName(), getFilter(), isIgnoreExceptions(), manager); + } + + } + +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraManager.java ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraManager.java b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraManager.java new file mode 100644 index 0000000..cfbc528 --- /dev/null +++ b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraManager.java @@ -0,0 +1,218 @@ +/* + * 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.logging.log4j.nosql.appender.cassandra; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.datastax.driver.core.BatchStatement; +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.ManagerFactory; +import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager; +import org.apache.logging.log4j.core.appender.db.ColumnMapping; +import org.apache.logging.log4j.core.config.plugins.convert.DateTypeConverter; +import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters; +import org.apache.logging.log4j.core.net.SocketAddress; +import org.apache.logging.log4j.spi.ThreadContextMap; +import org.apache.logging.log4j.spi.ThreadContextStack; +import org.apache.logging.log4j.util.ReadOnlyStringMap; +import org.apache.logging.log4j.util.Strings; + +/** + * Manager for a Cassandra appender instance. + */ +public class CassandraManager extends AbstractDatabaseManager { + + private static final int DEFAULT_PORT = 9042; + + private final Cluster cluster; + private final String keyspace; + private final String insertQueryTemplate; + private final List<ColumnMapping> columnMappings; + private final BatchStatement batchStatement; + // re-usable argument binding array + private final Object[] values; + + private Session session; + private PreparedStatement preparedStatement; + + private CassandraManager(final String name, final int bufferSize, final Cluster cluster, + final String keyspace, final String insertQueryTemplate, + final List<ColumnMapping> columnMappings, final BatchStatement batchStatement) { + super(name, bufferSize); + this.cluster = cluster; + this.keyspace = keyspace; + this.insertQueryTemplate = insertQueryTemplate; + this.columnMappings = columnMappings; + this.batchStatement = batchStatement; + this.values = new Object[columnMappings.size()]; + } + + @Override + protected void startupInternal() throws Exception { + session = cluster.connect(keyspace); + preparedStatement = session.prepare(insertQueryTemplate); + } + + @Override + protected boolean shutdownInternal() throws Exception { + session.close(); + cluster.close(); + return true; + } + + @Override + protected void connectAndStart() { + // a Session automatically manages connections for us + } + + @Override + protected void writeInternal(final LogEvent event) { + for (int i = 0; i < columnMappings.size(); i++) { + final ColumnMapping columnMapping = columnMappings.get(i); + if (ThreadContextMap.class.isAssignableFrom(columnMapping.getType()) + || ReadOnlyStringMap.class.isAssignableFrom(columnMapping.getType())) { + values[i] = event.getContextData().toMap(); + } else if (ThreadContextStack.class.isAssignableFrom(columnMapping.getType())) { + values[i] = event.getContextStack().asList(); + } else if (Date.class.isAssignableFrom(columnMapping.getType())) { + values[i] = DateTypeConverter.fromMillis(event.getTimeMillis(), columnMapping.getType().asSubclass(Date.class)); + } else { + values[i] = TypeConverters.convert(columnMapping.getLayout().toSerializable(event), + columnMapping.getType(), null); + } + } + final BoundStatement boundStatement = preparedStatement.bind(values); + if (batchStatement == null) { + session.execute(boundStatement); + } else { + batchStatement.add(boundStatement); + } + } + + @Override + protected boolean commitAndClose() { + if (batchStatement != null) { + session.execute(batchStatement); + } + return true; + } + + public static CassandraManager getManager(final String name, final SocketAddress[] contactPoints, + final ColumnMapping[] columns, final boolean useTls, + final String clusterName, final String keyspace, final String table, + final String username, final String password, + final boolean useClockForTimestampGenerator, final int bufferSize, + final boolean batched, final BatchStatement.Type batchType) { + return getManager(name, + new FactoryData(contactPoints, columns, useTls, clusterName, keyspace, table, username, password, + useClockForTimestampGenerator, bufferSize, batched, batchType), CassandraManagerFactory.INSTANCE); + } + + private static class CassandraManagerFactory implements ManagerFactory<CassandraManager, FactoryData> { + + private static final CassandraManagerFactory INSTANCE = new CassandraManagerFactory(); + + @Override + public CassandraManager createManager(final String name, final FactoryData data) { + final Cluster.Builder builder = Cluster.builder() + .addContactPointsWithPorts(data.contactPoints) + .withClusterName(data.clusterName); + if (data.useTls) { + builder.withSSL(); + } + if (Strings.isNotBlank(data.username)) { + builder.withCredentials(data.username, data.password); + } + if (data.useClockForTimestampGenerator) { + builder.withTimestampGenerator(new ClockTimestampGenerator()); + } + final Cluster cluster = builder.build(); + + final StringBuilder sb = new StringBuilder("INSERT INTO ").append(data.table).append(" ("); + for (final ColumnMapping column : data.columns) { + sb.append(column.getName()).append(','); + } + sb.setCharAt(sb.length() - 1, ')'); + sb.append(" VALUES ("); + final List<ColumnMapping> columnMappings = new ArrayList<>(data.columns.length); + for (final ColumnMapping column : data.columns) { + if (Strings.isNotEmpty(column.getLiteralValue())) { + sb.append(column.getLiteralValue()); + } else { + sb.append('?'); + columnMappings.add(column); + } + sb.append(','); + } + sb.setCharAt(sb.length() - 1, ')'); + final String insertQueryTemplate = sb.toString(); + LOGGER.debug("Using CQL for appender {}: {}", name, insertQueryTemplate); + return new CassandraManager(name, data.getBufferSize(), cluster, data.keyspace, insertQueryTemplate, + columnMappings, data.batched ? new BatchStatement(data.batchType) : null); + } + } + + private static class FactoryData extends AbstractFactoryData { + private final InetSocketAddress[] contactPoints; + private final ColumnMapping[] columns; + private final boolean useTls; + private final String clusterName; + private final String keyspace; + private final String table; + private final String username; + private final String password; + private final boolean useClockForTimestampGenerator; + private final boolean batched; + private final BatchStatement.Type batchType; + + private FactoryData(final SocketAddress[] contactPoints, final ColumnMapping[] columns, final boolean useTls, + final String clusterName, final String keyspace, final String table, final String username, + final String password, final boolean useClockForTimestampGenerator, final int bufferSize, + final boolean batched, final BatchStatement.Type batchType) { + super(bufferSize); + this.contactPoints = convertAndAddDefaultPorts(contactPoints); + this.columns = columns; + this.useTls = useTls; + this.clusterName = clusterName; + this.keyspace = keyspace; + this.table = table; + this.username = username; + this.password = password; + this.useClockForTimestampGenerator = useClockForTimestampGenerator; + this.batched = batched; + this.batchType = batchType; + } + + private static InetSocketAddress[] convertAndAddDefaultPorts(final SocketAddress... socketAddresses) { + final InetSocketAddress[] inetSocketAddresses = new InetSocketAddress[socketAddresses.length]; + for (int i = 0; i < inetSocketAddresses.length; i++) { + final SocketAddress socketAddress = socketAddresses[i]; + inetSocketAddresses[i] = socketAddress.getPort() == 0 + ? new InetSocketAddress(socketAddress.getAddress(), DEFAULT_PORT) + : socketAddress.getSocketAddress(); + } + return inetSocketAddresses; + } + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/ClockTimestampGenerator.java ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/ClockTimestampGenerator.java b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/ClockTimestampGenerator.java new file mode 100644 index 0000000..06758dd --- /dev/null +++ b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/ClockTimestampGenerator.java @@ -0,0 +1,34 @@ +/* + * 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.logging.log4j.nosql.appender.cassandra; + +import com.datastax.driver.core.TimestampGenerator; +import org.apache.logging.log4j.core.util.Clock; +import org.apache.logging.log4j.core.util.ClockFactory; + +/** + * A {@link TimestampGenerator} implementation using the configured {@link Clock}. + */ +public class ClockTimestampGenerator implements TimestampGenerator { + + private final Clock clock = ClockFactory.getClock(); + + @Override + public long next() { + return clock.currentTimeMillis(); + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/package-info.java ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/package-info.java b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/package-info.java new file mode 100644 index 0000000..79107c9 --- /dev/null +++ b/log4j-cassandra/src/main/java/org/apache/logging/log4j/nosql/appender/cassandra/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 appender plugin and supporting classes for Apache Cassandra. + * + * @see <a href="https://logging.apache.org/log4j/2.x/manual/appenders.html#CassandraAppender">Cassandra Appender manual</a> + * @since 2.8 + */ +package org.apache.logging.log4j.nosql.appender.cassandra; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/site/markdown/index.md.vm ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/site/markdown/index.md.vm b/log4j-cassandra/src/site/markdown/index.md.vm new file mode 100644 index 0000000..8a9c70b --- /dev/null +++ b/log4j-cassandra/src/site/markdown/index.md.vm @@ -0,0 +1,24 @@ +<!-- vim: set syn=markdown : --> +<!-- + 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. +--> +#set($h1='#') +#set($h2='##') +## TODO: use properties for dynamic dependency versions + +$h1 Cassandra Appender + +The Cassandra Appender allow applications to send events to Apache Cassandra repositories. http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/site/site.xml ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/site/site.xml b/log4j-cassandra/src/site/site.xml new file mode 100644 index 0000000..7c51066 --- /dev/null +++ b/log4j-cassandra/src/site/site.xml @@ -0,0 +1,52 @@ +<!-- + 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. + +--> +<project name="Log4j Cassandra Appender" + xmlns="http://maven.apache.org/DECORATION/1.4.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/DECORATION/1.4.0 http://maven.apache.org/xsd/decoration-1.4.0.xsd"> + <body> + <links> + <item name="Apache" href="http://www.apache.org/" /> + <item name="Logging Services" href="http://logging.apache.org/"/> + <item name="Log4j" href="../index.html"/> + </links> + + <!-- Component-specific reports --> + <menu ref="reports"/> + + <!-- Overall Project Info --> + <menu name="Log4j Project Information" img="icon-info-sign"> + <item name="Dependencies" href="../dependencies.html" /> + <item name="Dependency Convergence" href="../dependency-convergence.html" /> + <item name="Dependency Management" href="../dependency-management.html" /> + <item name="Project Team" href="../team-list.html" /> + <item name="Mailing Lists" href="../mail-lists.html" /> + <item name="Issue Tracking" href="../issue-tracking.html" /> + <item name="Project License" href="../license.html" /> + <item name="Source Repository" href="../source-repository.html" /> + <item name="Project Summary" href="../project-summary.html" /> + </menu> + + <menu name="Log4j Project Reports" img="icon-cog"> + <item name="Changes Report" href="../changes-report.html" /> + <item name="JIRA Report" href="../jira-report.html" /> + <item name="Surefire Report" href="../surefire-report.html" /> + <item name="RAT Report" href="../rat-report.html" /> + </menu> + </body> +</project> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppenderIT.java ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppenderIT.java b/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppenderIT.java new file mode 100644 index 0000000..caf72d5 --- /dev/null +++ b/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraAppenderIT.java @@ -0,0 +1,96 @@ +/* + * 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.logging.log4j.nosql.appender.cassandra; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.MarkerManager; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.categories.Appenders; +import org.apache.logging.log4j.junit.LoggerContextRule; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.RuleChain; + +import static org.junit.Assert.*; + +/** + * Integration test for CassandraAppender. + */ +@Category(Appenders.Cassandra.class) +public class CassandraAppenderIT { + + private static final String DDL = "CREATE TABLE logs (" + + "id timeuuid PRIMARY KEY," + + "timeid timeuuid," + + "message text," + + "level text," + + "marker text," + + "logger text," + + "timestamp timestamp," + + "mdc map<text,text>," + + "ndc list<text>" + + ")"; + + private static final LoggerContextRule CTX = new LoggerContextRule("CassandraAppenderTest.xml"); + private static final CassandraRule CASSANDRA = new CassandraRule("test", DDL); + + @ClassRule + public static RuleChain rules = RuleChain.outerRule(CASSANDRA).around(CTX); + + @Test + public void appendManyEvents() throws Exception { + final Logger logger = CTX.getLogger(); + ThreadContext.put("test", "mdc"); + ThreadContext.push("ndc"); + for (int i = 0; i < 20; i++) { + logger.info(MarkerManager.getMarker("MARKER"), "Test log message"); + } + ThreadContext.clearAll(); + + TimeUnit.SECONDS.sleep(3); + + int i = 0; + try (final Session session = CASSANDRA.connect()) { + for (final Row row : session.execute("SELECT * FROM logs")) { + assertNotNull(row.get("id", UUID.class)); + assertNotNull(row.get("timeid", UUID.class)); + assertNotNull(row.get("timestamp", Date.class)); + assertEquals("Test log message", row.getString("message")); + assertEquals("MARKER", row.getString("marker")); + assertEquals("INFO", row.getString("level")); + assertEquals(getClass().getName(), row.getString("logger")); + final Map<String, String> mdc = row.getMap("mdc", String.class, String.class); + assertEquals(1, mdc.size()); + assertEquals("mdc", mdc.get("test")); + final List<String> ndc = row.getList("ndc", String.class); + assertEquals(1, ndc.size()); + assertEquals("ndc", ndc.get(0)); + ++i; + } + } + assertEquals(20, i); + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraRule.java ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraRule.java b/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraRule.java new file mode 100644 index 0000000..36e2672 --- /dev/null +++ b/log4j-cassandra/src/test/java/org/apache/logging/log4j/nosql/appender/cassandra/CassandraRule.java @@ -0,0 +1,141 @@ +/* + * 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.logging.log4j.nosql.appender.cassandra; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.Permission; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadFactory; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; +import org.apache.cassandra.service.CassandraDaemon; +import org.apache.logging.log4j.LoggingException; +import org.apache.logging.log4j.core.util.Cancellable; +import org.apache.logging.log4j.core.util.Closer; +import org.apache.logging.log4j.core.util.Log4jThreadFactory; +import org.apache.logging.log4j.util.PropertiesUtil; +import org.junit.rules.ExternalResource; + +/** + * JUnit rule to set up and tear down a Cassandra database instance. + */ +public class CassandraRule extends ExternalResource { + + private static final ThreadFactory THREAD_FACTORY = Log4jThreadFactory.createThreadFactory("Cassandra"); + + private final CountDownLatch latch = new CountDownLatch(1); + private final Cancellable embeddedCassandra = new EmbeddedCassandra(latch); + private final String keyspace; + private final String tableDdl; + private Cluster cluster; + + public CassandraRule(final String keyspace, final String tableDdl) { + this.keyspace = keyspace; + this.tableDdl = tableDdl; + } + + public Cluster getCluster() { + return cluster; + } + + public Session connect() { + return cluster.connect(keyspace); + } + + @Override + protected void before() throws Throwable { + final Path root = Files.createTempDirectory("cassandra"); + Files.createDirectories(root.resolve("data")); + final Path config = root.resolve("cassandra.yml"); + Files.copy(getClass().getResourceAsStream("/cassandra.yaml"), config); + System.setProperty("cassandra.config", "file:" + config.toString()); + System.setProperty("cassandra.storagedir", root.toString()); + System.setProperty("cassandra-foreground", "true"); // prevents Cassandra from closing stdout/stderr + THREAD_FACTORY.newThread(embeddedCassandra).start(); + latch.await(); + cluster = Cluster.builder().addContactPoints(InetAddress.getLoopbackAddress()).build(); + try (final Session session = cluster.connect()) { + session.execute("CREATE KEYSPACE " + keyspace + " WITH REPLICATION = " + + "{ 'class': 'SimpleStrategy', 'replication_factor': 2 };"); + } + try (final Session session = connect()) { + session.execute(tableDdl); + } + } + + @Override + protected void after() { + Closer.closeSilently(cluster); + embeddedCassandra.cancel(); + } + + private static class EmbeddedCassandra implements Cancellable { + + private final CassandraDaemon daemon = new CassandraDaemon(); + private final CountDownLatch latch; + + private EmbeddedCassandra(final CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void cancel() { + // LOG4J2-1850 Cassandra on Windows calls System.exit in the daemon stop method + if (PropertiesUtil.getProperties().isOsWindows()) { + cancelOnWindows(); + } else { + daemon.stop(); + } + } + + private void cancelOnWindows() { + final SecurityManager currentSecurityManager = System.getSecurityManager(); + try { + final SecurityManager securityManager = new SecurityManager() { + @Override + public void checkPermission(final Permission permission) { + final String permissionName = permission.getName(); + if (permissionName != null && permissionName.startsWith("exitVM")) { + throw new SecurityException("test"); + } + } + }; + System.setSecurityManager(securityManager); + daemon.stop(); + } catch (final SecurityException ex) { + // ignore + } finally { + System.setSecurityManager(currentSecurityManager); + } + } + + @Override + public void run() { + try { + daemon.init(null); + } catch (final IOException e) { + throw new LoggingException("Cannot initialize embedded Cassandra instance", e); + } + daemon.start(); + latch.countDown(); + } + } +} http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/d3f9719f/log4j-cassandra/src/test/resources/CassandraAppenderTest.xml ---------------------------------------------------------------------- diff --git a/log4j-cassandra/src/test/resources/CassandraAppenderTest.xml b/log4j-cassandra/src/test/resources/CassandraAppenderTest.xml new file mode 100644 index 0000000..b3956d1 --- /dev/null +++ b/log4j-cassandra/src/test/resources/CassandraAppenderTest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + +--> +<Configuration name="CassandraAppenderTest"> + <Appenders> + <Cassandra name="Cassandra" clusterName="Test Cluster" keyspace="test" table="logs" bufferSize="10" batched="true"> + <SocketAddress host="localhost" port="9042"/> + <ColumnMapping name="id" pattern="%uuid{TIME}" type="java.util.UUID"/> + <ColumnMapping name="timeid" literal="now()"/> + <ColumnMapping name="message" pattern="%message"/> + <ColumnMapping name="level" pattern="%level"/> + <ColumnMapping name="marker" pattern="%marker"/> + <ColumnMapping name="logger" pattern="%logger"/> + <ColumnMapping name="timestamp" type="java.util.Date"/> + <ColumnMapping name="mdc" type="org.apache.logging.log4j.spi.ThreadContextMap"/> + <ColumnMapping name="ndc" type="org.apache.logging.log4j.spi.ThreadContextStack"/> + </Cassandra> + </Appenders> + <Loggers> + <Logger name="org.apache.logging.log4j.nosql.appender.cassandra" level="DEBUG"> + <AppenderRef ref="Cassandra"/> + </Logger> + <Root level="ERROR"/> + </Loggers> +</Configuration> \ No newline at end of file