Thanks for starting this! My idea in mind when I wrote the ticket was to
make the actual client pluggable so that users could use HttpURLConnection
(no dependencies), Apache HttpClient, Netty-HTTP, etc.

On 4 May 2017 at 07:49, <mi...@apache.org> wrote:

> Repository: logging-log4j2
> Updated Branches:
>   refs/heads/LOG4J2-1442 [created] 0af515f3b
>
>
> LOG4J2-1442 Generic HTTP appender
>
>
> Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo
> Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/
> commit/410f9d36
> Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/410f9d36
> Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/410f9d36
>
> Branch: refs/heads/LOG4J2-1442
> Commit: 410f9d360eabcdc6949c75973b786c9e2cec9c66
> Parents: 8852cd1
> Author: Mikael Ståldal <mikael.stal...@magine.com>
> Authored: Thu May 4 14:21:59 2017 +0200
> Committer: Mikael Ståldal <mikael.stal...@magine.com>
> Committed: Thu May 4 14:45:04 2017 +0200
>
> ----------------------------------------------------------------------
>  .../log4j/core/appender/HttpAppender.java       | 161 +++++++++++++++++++
>  .../log4j/core/appender/HttpManager.java        |  82 ++++++++++
>  .../log4j/core/appender/HttpAppenderTest.java   |  52 ++++++
>  .../src/test/resources/HttpAppenderTest.xml     |  43 +++++
>  src/changes/changes.xml                         |   3 +
>  src/site/site.xml                               |   1 +
>  src/site/xdoc/manual/appenders.xml              |  78 +++++++++
>  7 files changed, 420 insertions(+)
> ----------------------------------------------------------------------
>
>
> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/
> 410f9d36/log4j-core/src/main/java/org/apache/logging/log4j/
> core/appender/HttpAppender.java
> ----------------------------------------------------------------------
> diff --git 
> a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/HttpAppender.java
> b/log4j-core/src/main/java/org/apache/logging/log4j/core/
> appender/HttpAppender.java
> new file mode 100644
> index 0000000..e0f1b27
> --- /dev/null
> +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/
> appender/HttpAppender.java
> @@ -0,0 +1,161 @@
> +/*
> + * 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.core.appender;
> +
> +import java.io.IOException;
> +import java.io.Serializable;
> +import java.util.Objects;
> +import java.util.concurrent.TimeUnit;
> +
> +import org.apache.logging.log4j.core.Appender;
> +import org.apache.logging.log4j.core.Filter;
> +import org.apache.logging.log4j.core.Layout;
> +import org.apache.logging.log4j.core.LogEvent;
> +import org.apache.logging.log4j.core.config.Node;
> +import org.apache.logging.log4j.core.config.Property;
> +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;
> +
> +/**
> + * Sends log events over HTTP.
> + */
> +@Plugin(name = "Http", category = Node.CATEGORY, elementType =
> Appender.ELEMENT_TYPE, printObject = true)
> +public final class HttpAppender extends AbstractAppender {
> +
> +    /**
> +     * Builds HttpAppender instances.
> +     * @param <B> The type to build
> +     */
> +    public static class Builder<B extends Builder<B>> extends
> AbstractAppender.Builder<B>
> +            implements 
> org.apache.logging.log4j.core.util.Builder<HttpAppender>
> {
> +
> +        @PluginBuilderAttribute
> +        @Required(message = "No URL provided for HttpAppender")
> +        private String url;
> +
> +        @PluginBuilderAttribute
> +        private String method = "POST";
> +
> +        @PluginBuilderAttribute
> +        private int connectTimeoutMillis = 0;
> +
> +        @PluginBuilderAttribute
> +        private int readTimeoutMillis = 0;
> +
> +        @PluginElement("Headers")
> +        private Property[] headers;
> +
> +        @Override
> +        public HttpAppender build() {
> +            final HttpManager httpManager = new
> HttpManager(getConfiguration(), getConfiguration().getLoggerContext(),
> +                getName(), url, method, connectTimeoutMillis,
> readTimeoutMillis, headers);
> +            return new HttpAppender(getName(), getLayout(), getFilter(),
> isIgnoreExceptions(), httpManager);
> +        }
> +
> +        public String getUrl() {
> +            return url;
> +        }
> +
> +        public String getMethod() {
> +            return method;
> +        }
> +
> +        public int getConnectTimeoutMillis() {
> +            return connectTimeoutMillis;
> +        }
> +
> +        public int getReadTimeoutMillis() {
> +            return readTimeoutMillis;
> +        }
> +
> +        public Property[] getHeaders() {
> +            return headers;
> +        }
> +
> +        public B setUrl(final String url) {
> +            this.url = url;
> +            return asBuilder();
> +        }
> +
> +        public B setMethod(final String method) {
> +            this.method = method;
> +            return asBuilder();
> +        }
> +
> +        public B setConnectTimeoutMillis(int connectTimeoutMillis) {
> +            this.connectTimeoutMillis = connectTimeoutMillis;
> +            return asBuilder();
> +        }
> +
> +        public B setReadTimeoutMillis(int readTimeoutMillis) {
> +            this.readTimeoutMillis = readTimeoutMillis;
> +            return asBuilder();
> +        }
> +
> +        public B setHeaders(final Property[] headers) {
> +            this.headers = headers;
> +            return asBuilder();
> +        }
> +    }
> +
> +    /**
> +     * @return a builder for a HttpAppender.
> +     */
> +    @PluginBuilderFactory
> +    public static <B extends Builder<B>> B newBuilder() {
> +        return new Builder<B>().asBuilder();
> +    }
> +
> +    private final HttpManager manager;
> +
> +    private HttpAppender(final String name, final Layout<? extends
> Serializable> layout, final Filter filter,
> +                         final boolean ignoreExceptions, final
> HttpManager manager) {
> +        super(name, filter, layout, ignoreExceptions);
> +        Objects.requireNonNull(layout, "layout");
> +        this.manager = Objects.requireNonNull(manager, "manager");
> +    }
> +
> +    @Override
> +    public void append(final LogEvent event) {
> +        try {
> +            manager.send(getLayout(), event);
> +        } catch (final Exception e) {
> +            error("Unable to send HTTP in appender [" + getName() + "]",
> event, e);
> +        }
> +    }
> +
> +    @Override
> +    public boolean stop(final long timeout, final TimeUnit timeUnit) {
> +        setStopping();
> +        boolean stopped = super.stop(timeout, timeUnit, false);
> +        stopped &= manager.stop(timeout, timeUnit);
> +        setStopped();
> +        return stopped;
> +    }
> +
> +    @Override
> +    public String toString() {
> +        return "HttpAppender{" +
> +            "name=" + getName() +
> +            ", state=" + getState() +
> +            '}';
> +    }
> +}
>
> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/
> 410f9d36/log4j-core/src/main/java/org/apache/logging/log4j/
> core/appender/HttpManager.java
> ----------------------------------------------------------------------
> diff --git 
> a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/HttpManager.java
> b/log4j-core/src/main/java/org/apache/logging/log4j/core/
> appender/HttpManager.java
> new file mode 100644
> index 0000000..8f69659
> --- /dev/null
> +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/
> appender/HttpManager.java
> @@ -0,0 +1,82 @@
> +/*
> + * 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.core.appender;
> +
> +import java.io.IOException;
> +import java.io.OutputStream;
> +import java.net.HttpURLConnection;
> +import java.net.MalformedURLException;
> +import java.net.URL;
> +import java.util.Objects;
> +
> +import org.apache.logging.log4j.core.Layout;
> +import org.apache.logging.log4j.core.LogEvent;
> +import org.apache.logging.log4j.core.LoggerContext;
> +import org.apache.logging.log4j.core.config.Configuration;
> +import org.apache.logging.log4j.core.config.ConfigurationException;
> +import org.apache.logging.log4j.core.config.Property;
> +
> +public class HttpManager extends AbstractManager {
> +
> +    private final Configuration configuration;
> +    private final URL url;
> +    private final String method;
> +    private final int connectTimeoutMillis;
> +    private final int readTimeoutMillis;
> +    private final Property[] headers;
> +
> +    public HttpManager(final Configuration configuration, LoggerContext
> loggerContext, final String name,
> +                       final String url, final String method, final int
> connectTimeoutMillis, final int readTimeoutMillis,
> +                       final Property[] headers) {
> +        super(loggerContext, name);
> +        this.configuration = Objects.requireNonNull(configuration);
> +        try {
> +            this.url = new URL(url);
> +        } catch (MalformedURLException e) {
> +            throw new ConfigurationException(e);
> +        }
> +        this.method = Objects.requireNonNull(method, "method");
> +        this.connectTimeoutMillis = connectTimeoutMillis;
> +        this.readTimeoutMillis = readTimeoutMillis;
> +        this.headers = headers != null ? headers : new Property[0];
> +    }
> +
> +    public void send(final Layout<?> layout, final LogEvent event) throws
> IOException {
> +        HttpURLConnection urlConnection = (HttpURLConnection)url.
> openConnection();
> +        urlConnection.setAllowUserInteraction(false);
> +        urlConnection.setDoOutput(true);
> +        urlConnection.setDoInput(true);
> +        urlConnection.setRequestMethod(method);
> +        if (connectTimeoutMillis > 0) urlConnection.setConnectTimeout(
> connectTimeoutMillis);
> +        if (readTimeoutMillis > 0) urlConnection.setReadTimeout(
> readTimeoutMillis);
> +        if (layout.getContentType() != null) urlConnection.
> setRequestProperty("Content-Type", layout.getContentType());
> +        for (Property header : headers) {
> +            urlConnection.setRequestProperty(
> +                header.getName(),
> +                header.isValueNeedsLookup() ? configuration.
> getStrSubstitutor().replace(event, header.getValue()) :
> header.getValue());
> +        }
> +        byte[] msg = layout.toByteArray(event);
> +        urlConnection.setFixedLengthStreamingMode(msg.length);
> +        urlConnection.connect();
> +        try (OutputStream os = urlConnection.getOutputStream()) {
> +            os.write(msg);
> +        }
> +        urlConnection.getInputStream().close();
> +    }
> +
> +}
>
> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/
> 410f9d36/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/
> HttpAppenderTest.java
> ----------------------------------------------------------------------
> diff --git 
> a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/HttpAppenderTest.java
> b/log4j-core/src/test/java/org/apache/logging/log4j/core/
> appender/HttpAppenderTest.java
> new file mode 100644
> index 0000000..98120a1
> --- /dev/null
> +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/
> appender/HttpAppenderTest.java
> @@ -0,0 +1,52 @@
> +package org.apache.logging.log4j.core.appender;
> +
> +import org.apache.logging.log4j.Level;
> +import org.apache.logging.log4j.core.Appender;
> +import org.apache.logging.log4j.core.impl.Log4jLogEvent;
> +import org.apache.logging.log4j.junit.LoggerContextRule;
> +import org.apache.logging.log4j.message.SimpleMessage;
> +import org.junit.Rule;
> +import org.junit.Test;
> +
> +// TODO this test requires manual verification
> +public class HttpAppenderTest {
> +
> +    private static final String LOG_MESSAGE = "Hello, world!";
> +
> +    private static Log4jLogEvent createLogEvent() {
> +        return Log4jLogEvent.newBuilder()
> +            .setLoggerName(HttpAppenderTest.class.getName())
> +            .setLoggerFqcn(HttpAppenderTest.class.getName())
> +            .setLevel(Level.INFO)
> +            .setMessage(new SimpleMessage(LOG_MESSAGE))
> +            .build();
> +    }
> +
> +    @Rule
> +    public LoggerContextRule ctx = new LoggerContextRule("
> HttpAppenderTest.xml");
> +
> +    @Test
> +    public void testAppendSuccess() throws Exception {
> +        final Appender appender = ctx.getRequiredAppender("HttpSuccess");
> +        appender.append(createLogEvent());
> +    }
> +
> +    @Test
> +    public void testAppendErrorIgnore() throws Exception {
> +        final Appender appender = ctx.getRequiredAppender("
> HttpErrorIgnore");
> +        appender.append(createLogEvent());
> +    }
> +
> +    @Test(expected = AppenderLoggingException.class)
> +    public void testAppendError() throws Exception {
> +        final Appender appender = ctx.getRequiredAppender("HttpError");
> +        appender.append(createLogEvent());
> +    }
> +
> +    @Test
> +    public void testAppendSubst() throws Exception {
> +        final Appender appender = ctx.getRequiredAppender("HttpSubst");
> +        appender.append(createLogEvent());
> +    }
> +
> +}
> \ No newline at end of file
>
> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/
> 410f9d36/log4j-core/src/test/resources/HttpAppenderTest.xml
> ----------------------------------------------------------------------
> diff --git a/log4j-core/src/test/resources/HttpAppenderTest.xml
> b/log4j-core/src/test/resources/HttpAppenderTest.xml
> new file mode 100644
> index 0000000..30edaa0
> --- /dev/null
> +++ b/log4j-core/src/test/resources/HttpAppenderTest.xml
> @@ -0,0 +1,43 @@
> +<?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="HttpAppenderTest" status="WARN">
> +  <Appenders>
> +    <Http name="HttpSuccess" url="http://localhost:9200/test/log4j/";>
> +      <Property name="X-Test" value="header value" />
> +      <JsonLayout properties="true"/>
> +    </Http>
> +    <Http name="HttpErrorIgnore" url="http://localhost:9200/test/log4j/";
> method="PUT">
> +      <JsonLayout properties="true"/>
> +    </Http>
> +    <Http name="HttpError" url="http://localhost:9200/test/log4j/";
> method="PUT" ignoreExceptions="false">
> +      <JsonLayout properties="true"/>
> +    </Http>
> +    <Http name="HttpSubst" url="http://localhost:9200/test/log4j/";>
> +      <Property name="X-Test" value="$${java:runtime}" />
> +      <JsonLayout properties="true"/>
> +    </Http>
> +  </Appenders>
> +  <Loggers>
> +    <Root level="info">
> +      <AppenderRef ref="HttpSuccess"/>
> +      <AppenderRef ref="HttpErrorIgnore"/>
> +      <AppenderRef ref="HttpError"/>
> +      <AppenderRef ref="HttpSubst"/>
> +    </Root>
> +  </Loggers>
> +</Configuration>
> \ No newline at end of file
>
> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/
> 410f9d36/src/changes/changes.xml
> ----------------------------------------------------------------------
> diff --git a/src/changes/changes.xml b/src/changes/changes.xml
> index 379c21e..d0f8133 100644
> --- a/src/changes/changes.xml
> +++ b/src/changes/changes.xml
> @@ -31,6 +31,9 @@
>           - "remove" - Removed
>      -->
>      <release version="2.9.0" date="2017-MM-DD" description="GA Release
> 2.9.0">
> +      <action issue="LOG4J2-1442" dev="mikes" type="add">
> +        Generic HTTP appender.
> +      </action>
>        <action issue="LOG4J2-1854" dev="mikes" type="add" due-to="Xavier
> Jodoin">
>          Support null byte delimiter in GelfLayout.
>        </action>
>
> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/
> 410f9d36/src/site/site.xml
> ----------------------------------------------------------------------
> diff --git a/src/site/site.xml b/src/site/site.xml
> index e380c82..aa161fe 100644
> --- a/src/site/site.xml
> +++ b/src/site/site.xml
> @@ -134,6 +134,7 @@
>          <item name="JDBC" href="/manual/appenders.html#JDBCAppender"/>
>          <item name="JMS" href="/manual/appenders.html#JMSAppender"/>
>          <item name="JPA" href="/manual/appenders.html#JPAAppender"/>
> +        <item name="HTTP" href="/manual/appenders.html#HttpAppender"/>
>          <item name="Kafka" href="/manual/appenders.html#KafkaAppender"/>
>          <item name="Memory Mapped File" href="/manual/appenders.html#
> MemoryMappedFileAppender"/>
>          <item name="NoSQL" href="/manual/appenders.html#NoSQLAppender"/>
>
> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/
> 410f9d36/src/site/xdoc/manual/appenders.xml
> ----------------------------------------------------------------------
> diff --git a/src/site/xdoc/manual/appenders.xml b/src/site/xdoc/manual/
> appenders.xml
> index 28d9aa4..f7721df 100644
> --- a/src/site/xdoc/manual/appenders.xml
> +++ b/src/site/xdoc/manual/appenders.xml
> @@ -1538,6 +1538,84 @@ public class JpaLogEntity extends
> AbstractLogEventWrapperEntity {
>      ...
>  }]]></pre>
>          </subsection>
> +        <a name="HttpAppender"/>
> +        <subsection name="HttpAppender">
> +          <p>
> +            The HttpAppender sends log events over HTTP. A Layout must be
> provided to format the LogEvent.
> +          </p>
> +          <p>
> +            Will set the <code>Content-Type</code> header according to
> the layout. Additional headers can be specified
> +            with embedded Property elements.
> +          </p>
> +          <table>
> +            <caption align="top">HttpAppender Parameters</caption>
> +            <tr>
> +              <th>Parameter Name</th>
> +              <th>Type</th>
> +              <th>Description</th>
> +            </tr>
> +            <tr>
> +              <td>name</td>
> +              <td>String</td>
> +              <td>The name of the Appender.</td>
> +            </tr>
> +            <tr>
> +              <td>filter</td>
> +              <td>Filter</td>
> +              <td>A Filter to determine if the event should be handled by
> this Appender. More than one Filter
> +                may be used by using a CompositeFilter.</td>
> +            </tr>
> +            <tr>
> +              <td>layout</td>
> +              <td>Layout</td>
> +              <td>The Layout to use to format the LogEvent.</td>
> +            </tr>
> +            <tr>
> +              <td>url</td>
> +              <td>string</td>
> +              <td>The URL to use. The URL scheme must be "http" or
> "https".</td>
> +            </tr>
> +            <tr>
> +              <td>method</td>
> +              <td>string</td>
> +              <td>The HTTP method to use. Optional, default is
> "POST".</td>
> +            </tr>
> +            <tr>
> +              <td>connectTimeoutMillis</td>
> +              <td>integer</td>
> +              <td>The connect timeout in milliseconds. Optional, default
> is 0 (infinite timeout).</td>
> +            </tr>
> +            <tr>
> +              <td>readTimeoutMillis</td>
> +              <td>integer</td>
> +              <td>The socket read timeout in milliseconds. Optional,
> default is 0 (infinite timeout).</td>
> +            </tr>
> +            <tr>
> +              <td>headers</td>
> +              <td>Property[]</td>
> +              <td>Additional HTTP headers to use. The values support <a
> href="lookups.html">lookups</a></td>
> +            </tr>
> +            <tr>
> +              <td>ignoreExceptions</td>
> +              <td>boolean</td>
> +              <td>The default is <code>true</code>, causing exceptions
> encountered while appending events to be
> +                internally logged and then ignored. When set to
> <code>false</code> exceptions will be propagated to the
> +                caller, instead. You must set this to <code>false</code>
> when wrapping this Appender in a
> +                <a href="#FailoverAppender">FailoverAppender</a>.</td>
> +            </tr>
> +          </table>
> +          <p>
> +            Here is a sample HttpAppender configuration snippet:
> +          </p>
> +          <pre class="prettyprint linenums"><![CDATA[<?xml version="1.0"
> encoding="UTF-8"?>
> +  ...
> +  <Appenders>
> +    <Http name="Http" url="http://localhost:9200/test/log4j/";>
> +      <Property name="X-Java-Runtime" value="$${java:runtime}" />
> +      <JsonLayout properties="true"/>
> +    </Http>
> +  </Appenders>]]></pre>
> +        </subsection>
>          <a name="KafkaAppender"/>
>          <subsection name="KafkaAppender">
>            <p>
>
>


-- 
Matt Sicker <boa...@gmail.com>

Reply via email to