You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@felix.apache.org by Carsten Ziegeler <cz...@apache.org> on 2017/11/14 19:32:10 UTC

Re: svn commit: r1815249 - in /felix/trunk/http/jetty: ./ src/main/java/org/apache/felix/http/jetty/internal/ src/test/java/org/apache/felix/http/jetty/internal/

Hi,

do we have a Jira issue covering this?

Could you please also apply the patch at the R7 branch so they don't get
out of sync?

Thanks

Carsten


Nbartlett wrote
> Author: nbartlett
> Date: Tue Nov 14 19:09:34 2017
> New Revision: 1815249
> 
> URL: http://svn.apache.org/viewvc?rev=1815249&view=rev
> Log:
> Implement HTTP request logging. This closes #127
> 
> Added:
>     felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
>     felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
>     felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
>     felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
> Modified:
>     felix/trunk/http/jetty/pom.xml
>     felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
>     felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
> 
> Modified: felix/trunk/http/jetty/pom.xml
> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/pom.xml?rev=1815249&r1=1815248&r2=1815249&view=diff
> ==============================================================================
> --- felix/trunk/http/jetty/pom.xml (original)
> +++ felix/trunk/http/jetty/pom.xml Tue Nov 14 19:09:34 2017
> @@ -38,6 +38,8 @@
>      </scm>
>      
>      <properties>
> +        <!-- Skip because of problems with Java 8 -->
> +        <animal.sniffer.skip>true</animal.sniffer.skip>
>          <felix.java.version>8</felix.java.version>
>          <jetty.version>9.3.22.v20171030</jetty.version>
>      </properties>
> 
> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java?rev=1815249&view=auto
> ==============================================================================
> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java (added)
> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java Tue Nov 14 19:09:34 2017
> @@ -0,0 +1,97 @@
> +/*
> + * 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.felix.http.jetty.internal;
> +
> +import org.apache.felix.http.base.internal.logger.SystemLogger;
> +import org.eclipse.jetty.server.*;
> +import org.osgi.framework.BundleContext;
> +import org.osgi.framework.ServiceRegistration;
> +
> +import java.io.File;
> +import java.io.IOException;
> +import java.nio.file.Files;
> +import java.nio.file.attribute.PosixFilePermission;
> +import java.nio.file.attribute.PosixFilePermissions;
> +import java.util.Dictionary;
> +import java.util.Hashtable;
> +
> +class FileRequestLog {
> +
> +    public static final String SVC_PROP_NAME = "name";
> +    public static final String DEFAULT_NAME = "file";
> +    public static final String SVC_PROP_FILEPATH = "filepath";
> +
> +    private final NCSARequestLog delegate;
> +    private final String logFilePath;
> +    private final String serviceName;
> +    private ServiceRegistration<RequestLog> registration = null;
> +
> +    FileRequestLog(JettyConfig config) {
> +        logFilePath = config.getRequestLogFilePath();
> +        serviceName = config.getRequestLogFileServiceName() != null ? config.getRequestLogFileServiceName() : DEFAULT_NAME;
> +        if (config.isRequestLogFileAsync()) {
> +            delegate = new AsyncNCSARequestLog(logFilePath);
> +        } else {
> +            delegate = new NCSARequestLog(logFilePath);
> +        }
> +
> +        delegate.setAppend(config.isRequestLogFileAppend());
> +        delegate.setRetainDays(config.getRequestLogFileRetainDays());
> +        delegate.setFilenameDateFormat(config.getRequestLogFilenameDateFormat());
> +        delegate.setExtended(config.isRequestLogFileExtended());
> +        delegate.setIgnorePaths(config.getRequestLogFileIgnorePaths());
> +        delegate.setLogCookies(config.isRequestLogFileLogCookies());
> +        delegate.setLogServer(config.isRequestLogFileLogServer());
> +        delegate.setLogLatency(config.isRequestLogFileLogLatency());
> +    }
> +
> +    synchronized void start(BundleContext context) throws IOException, IllegalStateException {
> +        File logFile = new File(logFilePath).getAbsoluteFile();
> +        File logFileDir = logFile.getParentFile();
> +        if (logFileDir != null && !logFileDir.isDirectory()) {
> +            SystemLogger.info("Creating directory " + logFileDir.getAbsolutePath());
> +            Files.createDirectories(logFileDir.toPath(), PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
> +        }
> +
> +        if (registration != null) {
> +            throw new IllegalStateException(getClass().getSimpleName() + " is already started");
> +        }
> +        try {
> +            delegate.start();
> +            Dictionary<String, Object> svcProps = new Hashtable<>();
> +            svcProps.put(SVC_PROP_NAME, serviceName);
> +            svcProps.put(SVC_PROP_FILEPATH, logFilePath);
> +            registration = context.registerService(RequestLog.class, delegate, svcProps);
> +        } catch (Exception e) {
> +            SystemLogger.error("Error starting File Request Log", e);
> +        }
> +    }
> +
> +    synchronized void stop() {
> +        try {
> +            if (registration != null) {
> +                registration.unregister();
> +            }
> +            delegate.stop();;
> +        } catch (Exception e) {
> +            SystemLogger.error("Error shutting down File Request Log", e);
> +        } finally {
> +            registration = null;
> +        }
> +    }
> +
> +}
> 
> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java?rev=1815249&r1=1815248&r2=1815249&view=diff
> ==============================================================================
> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java (original)
> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java Tue Nov 14 19:09:34 2017
> @@ -165,6 +165,51 @@ public final class JettyConfig
>      /** Felix specific property to set HTTP instance name. */
>      public static final String FELIX_HTTP_SERVICE_NAME = "org.apache.felix.http.name";
>  
> +    /** Felix specific property to configure a filter for RequestLog services */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILTER = "org.apache.felix.http.requestlog.filter";
> +
> +    /** Felix specific property to enable request logging to the OSGi Log Service */
> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE = "org.apache.felix.http.requestlog.osgi.enable";
> +
> +    /** Felix specific property to specify the published "name" property of the OSGi Log Service-base Request Log service. Allows server configs to filter on specific log services. */
> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME = "org.apache.felix.http.requestlog.osgi.name";
> +
> +    /** Felix specific property to control the level of the log messages generated by the OSGi Log Service-based request log. Values must correspond to the constants defined in the LogService interface, default is 3 "INFO". */
> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL = "org.apache.felix.http.requestlog.osgi.level";
> +
> +    /** Felix specific property to enable request logging to a file and provide the path to that file. Default is null meaning that the file log is disabled. */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_PATH = "org.apache.felix.http.requestlog.file.path";
> +
> +    /** Felix specific property to specify the published "name" property of the file-based RequestLog service. Allows server configs to filter on specific log services. */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME = "org.apache.felix.http.requestlog.file.name";
> +
> +    /** Felix specific property to enable file request logging to be asynchronous */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_ASYNC = "org.apache.felix.http.requestlog.file.async";
> +
> +    /** Felix specific property to enable request logging to append to the log file rather than overwriting */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_APPEND = "org.apache.felix.http.requestlog.file.append";
> +
> +    /** Felix specific property to specify the number of days the request log file is retained */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS = "org.apache.felix.http.requestlog.file.retaindays";
> +
> +    /** Felix specific property to specify the date format in request log file names */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT = "org.apache.felix.http.requestlog.file.dateformat";
> +
> +    /** Felix specific property to enable extended request logging to a named file */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED = "org.apache.felix.http.requestlog.file.extended";
> +
> +    /** Felix specific property to ignore matching paths in the request log file */
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS = "org.apache.felix.http.requestlog.file.ignorepaths";
> +
> +    /** Felix specific property to enable request logging cookies in the request log file*/
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES = "org.apache.felix.http.requestlog.file.logcookies";
> +
> +    /** Felix specific property to enable request logging the host name in the request log file*/
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER = "org.apache.felix.http.requestlog.file.logserver";
> +
> +    /** Felix specific property to enable request logging request processing time in the request log file*/
> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY = "org.apache.felix.http.requestlog.file.loglatency";
> +
>      /** Felix specific property to define custom properties for the http runtime service. */
>      public static final String FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX = "org.apache.felix.http.runtime.init.";
>  
> @@ -425,6 +470,66 @@ public final class JettyConfig
>      	return (String) getProperty(FELIX_HTTP_SERVICE_NAME);
>      }
>  
> +    public String getRequestLogFilter() {
> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILTER, null);
> +    }
> +
> +    public boolean isRequestLogOSGiEnabled() {
> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE, false);
> +    }
> +
> +    public String getRequestLogOSGiServiceName() {
> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME);
> +    }
> +
> +    public int getRequestLogOSGiLevel() {
> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL, 3); // 3 == LogService.LOG_INFO
> +    }
> +
> +    public String getRequestLogFilePath() {
> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_PATH, null);
> +    }
> +
> +    public String getRequestLogFileServiceName() {
> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME, "file");
> +    }
> +
> +    public boolean isRequestLogFileAsync() {
> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_ASYNC, false);
> +    }
> +
> +    public boolean isRequestLogFileAppend() {
> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_APPEND, true);
> +    }
> +
> +    public int getRequestLogFileRetainDays() {
> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS, 31);
> +    }
> +
> +    public String getRequestLogFilenameDateFormat() {
> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT, null);
> +    }
> +
> +    public boolean isRequestLogFileExtended() {
> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED, false);
> +    }
> +
> +    public String[] getRequestLogFileIgnorePaths() {
> +        return getStringArrayProperty(FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS, new String[0]);
> +    }
> +
> +    public boolean isRequestLogFileLogCookies() {
> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES, false);
> +    }
> +
> +    public boolean isRequestLogFileLogServer() {
> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER, false);
> +    }
> +
> +    public boolean isRequestLogFileLogLatency() {
> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY, false);
> +    }
> +
>      public void reset()
>      {
>          update(null);
> @@ -668,4 +773,5 @@ public final class JettyConfig
>              return dflt;
>          }
>      }
> +
>  }
> 
> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java?rev=1815249&r1=1815248&r2=1815249&view=diff
> ==============================================================================
> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java (original)
> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java Tue Nov 14 19:09:34 2017
> @@ -101,11 +101,15 @@ public final class JettyService extends
>      private volatile BundleTracker<Deployment> bundleTracker;
>      private volatile ServiceTracker<EventAdmin, EventAdmin> eventAdmintTracker;
>      private volatile ConnectorFactoryTracker connectorTracker;
> +    private volatile RequestLogTracker requestLogTracker;
> +    private volatile LogServiceRequestLog osgiRequestLog;
> +    private volatile FileRequestLog fileRequestLog;
>      private volatile LoadBalancerCustomizerFactoryTracker loadBalancerCustomizerTracker;
>      private volatile CustomizerWrapper customizerWrapper;
>      private volatile EventAdmin eventAdmin;
>      private boolean registerManagedService = true;
>  
> +
>      public JettyService(final BundleContext context,
>              final HttpServiceController controller)
>      {
> @@ -292,6 +296,22 @@ public final class JettyService extends
>              this.controller.getEventDispatcher().setActive(false);
>              this.controller.unregister();
>  
> +            if (this.fileRequestLog != null)
> +            {
> +                this.fileRequestLog.stop();
> +                this.fileRequestLog = null;
> +            }
> +            if (this.osgiRequestLog != null)
> +            {
> +                this.osgiRequestLog.unregister();
> +                this.osgiRequestLog = null;
> +            }
> +            if (this.requestLogTracker != null)
> +            {
> +                this.requestLogTracker.close();
> +                this.requestLogTracker = null;
> +            }
> +
>              if (this.connectorTracker != null)
>              {
>                  this.connectorTracker.close();
> @@ -413,6 +433,26 @@ public final class JettyService extends
>                  this.stopJetty();
>                  SystemLogger.error("Jetty stopped (no connectors available)", null);
>              }
> +
> +            try {
> +                this.requestLogTracker = new RequestLogTracker(this.context, this.config.getRequestLogFilter());
> +                this.requestLogTracker.open();
> +                this.server.setRequestLog(requestLogTracker);
> +            } catch (InvalidSyntaxException e) {
> +                SystemLogger.error("Invalid filter syntax in request log tracker", e);
> +            }
> +
> +            if (this.config.isRequestLogOSGiEnabled()) {
> +                this.osgiRequestLog = new LogServiceRequestLog(this.config);
> +                this.osgiRequestLog.register(this.context);
> +                SystemLogger.info("Directing Jetty request logs to the OSGi Log Service");
> +            }
> +
> +            if (this.config.getRequestLogFilePath() != null && !this.config.getRequestLogFilePath().isEmpty()) {
> +                this.fileRequestLog = new FileRequestLog(config);
> +                this.fileRequestLog.start(this.context);
> +                SystemLogger.info("Directing Jetty request logs to " + this.config.getRequestLogFilePath());
> +            }
>          }
>          else
>          {
> 
> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java?rev=1815249&view=auto
> ==============================================================================
> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java (added)
> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java Tue Nov 14 19:09:34 2017
> @@ -0,0 +1,80 @@
> +/*
> + * 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.felix.http.jetty.internal;
> +
> +import org.apache.felix.http.base.internal.logger.SystemLogger;
> +import org.eclipse.jetty.server.AbstractNCSARequestLog;
> +import org.eclipse.jetty.server.RequestLog;
> +import org.osgi.framework.BundleContext;
> +import org.osgi.framework.ServiceRegistration;
> +
> +import java.io.IOException;
> +import java.util.Dictionary;
> +import java.util.Hashtable;
> +import java.util.concurrent.atomic.AtomicReference;
> +
> +/**
> + * A RequestLog that logs to the OSGi LogService when present. Not registered by default.
> + */
> +class LogServiceRequestLog extends AbstractNCSARequestLog {
> +
> +    public static final String SVC_PROP_NAME = "name";
> +    public static final String DEFAULT_NAME = "osgi";
> +    public static final String PREFIX = "REQUEST: ";
> +
> +    private static final int DEFAULT_LOG_LEVEL = 3; // LogService.LOG_INFO
> +
> +    private final int logLevel;
> +    private final String serviceName;
> +
> +    private ServiceRegistration<RequestLog> registration;
> +
> +    LogServiceRequestLog(JettyConfig config) {
> +        this.serviceName = config.getRequestLogOSGiServiceName();
> +        this.logLevel = config.getRequestLogOSGiLevel();
> +    }
> +
> +    public synchronized void register(BundleContext context) throws IllegalStateException {
> +        if (registration != null) {
> +            throw new IllegalStateException(getClass().getSimpleName() + " already registered");
> +        }
> +        Dictionary<String, Object> svcProps = new Hashtable<>();
> +        svcProps.put(SVC_PROP_NAME, serviceName);
> +        this.registration = context.registerService(RequestLog.class, this, svcProps);
> +    }
> +
> +    public synchronized void unregister() {
> +        try {
> +            if (registration != null) {
> +                registration.unregister();;
> +            }
> +        } finally {
> +            registration = null;
> +        }
> +    }
> +
> +    @Override
> +    public void write(String s) throws IOException {
> +        SystemLogger.info(PREFIX + s);
> +    }
> +
> +    @Override
> +    protected boolean isEnabled() {
> +        return true;
> +    }
> +
> +}
> 
> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java?rev=1815249&view=auto
> ==============================================================================
> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java (added)
> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java Tue Nov 14 19:09:34 2017
> @@ -0,0 +1,106 @@
> +/*
> + * 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.felix.http.jetty.internal;
> +
> +import org.apache.felix.http.base.internal.logger.SystemLogger;
> +import org.eclipse.jetty.server.Request;
> +import org.eclipse.jetty.server.RequestLog;
> +import org.eclipse.jetty.server.Response;
> +import org.osgi.framework.*;
> +import org.osgi.util.tracker.ServiceTracker;
> +
> +import java.util.Map;
> +import java.util.concurrent.ConcurrentHashMap;
> +import java.util.concurrent.ConcurrentMap;
> +
> +/**
> + * An instance of Jetty's RequestLog that dispatches to registered RequestLog services in the service registry. A filter
> + * can be provided so that it only dispatches to selected services.
> + * <p>
> + * Unchecked exceptions from the RequestLog services are caught and logged to the OSGi LogService. to avoid flooding the
> + * LogService, we will remove a RequestLog service if it breaches a maximum number of errors (see {@link
> + * RequestLogTracker#MAX_ERROR_COUNT}). Once this happens we will stop dispatching to that service entirely until it is
> + * unregistered.
> + */
> +class RequestLogTracker extends ServiceTracker<RequestLog, RequestLog>  implements RequestLog {
> +
> +    private static final int MAX_ERROR_COUNT = 100;
> +
> +    private final ConcurrentMap<ServiceReference<?>, RequestLog> logSvcs = new ConcurrentHashMap<>();
> +    private final ConcurrentMap<ServiceReference<?>, Integer> naughtyStep = new ConcurrentHashMap<>();
> +
> +    RequestLogTracker(BundleContext context, String filter) throws InvalidSyntaxException {
> +        super(context, buildFilter(filter), null);
> +    }
> +
> +    private static Filter buildFilter(String inputFilter) throws InvalidSyntaxException {
> +        String objectClassFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, RequestLog.class.getName());
> +        String compositeFilter;
> +        if (inputFilter != null) {
> +            // Parse the input filter just for validation before we insert into ours.
> +            FrameworkUtil.createFilter(inputFilter);
> +            compositeFilter = "(&" + objectClassFilter + inputFilter + ")";
> +        } else {
> +            compositeFilter = objectClassFilter;
> +        }
> +        return FrameworkUtil.createFilter(compositeFilter);
> +    }
> +
> +    @Override
> +    public RequestLog addingService(ServiceReference<RequestLog> reference) {
> +        RequestLog logSvc = context.getService(reference);
> +        logSvcs.put(reference, logSvc);
> +        return logSvc;
> +    }
> +
> +    @Override
> +    public void removedService(ServiceReference<RequestLog> reference, RequestLog logSvc) {
> +        logSvcs.remove(reference);
> +        naughtyStep.remove(reference);
> +        context.ungetService(reference);
> +    }
> +
> +    @Override
> +    public void log(Request request, Response response) {
> +        for (Map.Entry<ServiceReference<?>, RequestLog> entry : logSvcs.entrySet()) {
> +            try {
> +                entry.getValue().log(request, response);
> +            } catch (Exception e) {
> +                processError(entry.getKey(), e);
> +            }
> +        }
> +    }
> +
> +    /**
> +     * Process an exception from a RequestLog service instance, and remove the service if it has reached the maximum
> +     * error limit.
> +     */
> +    private void processError(ServiceReference<?> reference, Exception e) {
> +        SystemLogger.error(reference, String.format("Error dispatching to request log service ID %d from bundle %s:%s",
> +                reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), e);
> +
> +        int naughty = naughtyStep.merge(reference, 1, Integer::sum);
> +        if (naughty >= MAX_ERROR_COUNT) {
> +            // We have reached (but not exceeded) the maximum, and the last error has been logged. Remove from the maps
> +            // so we will not invoke the service again.
> +            logSvcs.remove(reference);
> +            naughtyStep.remove(reference);
> +            SystemLogger.error(reference, String.format("RequestLog service ID %d from bundle %s:%s threw too many errors, it will no longer be invoked.",
> +                    reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), null);
> +        }
> +    }
> +}
> 
> Added: felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java?rev=1815249&view=auto
> ==============================================================================
> --- felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java (added)
> +++ felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java Tue Nov 14 19:09:34 2017
> @@ -0,0 +1,97 @@
> +/*
> + * 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.felix.http.jetty.internal;
> +
> +import org.eclipse.jetty.server.Request;
> +import org.eclipse.jetty.server.RequestLog;
> +import org.eclipse.jetty.server.Response;
> +import org.junit.Test;
> +import org.junit.runner.RunWith;
> +import org.mockito.Mock;
> +import org.mockito.runners.MockitoJUnitRunner;
> +import org.osgi.framework.*;
> +
> +import java.util.concurrent.atomic.AtomicInteger;
> +
> +import static org.junit.Assert.assertEquals;
> +import static org.mockito.Mockito.*;
> +
> +@RunWith(MockitoJUnitRunner.class)
> +public class RequestLogTrackerTest {
> +    
> +    @Mock
> +    BundleContext context;
> +    
> +    @Test
> +    public void testInvokeRequestLog() throws Exception {
> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
> +
> +        RequestLog mockRequestLog = mock(RequestLog.class);
> +
> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
> +
> +        // These invocations not passed through to the mock because it is not registered yet
> +        for (int i = 0; i < 10; i++)
> +            tracker.log(null, null);
> +
> +        tracker.addingService(mockSvcRef);
> +
> +        // These will pass through
> +        for (int i = 0; i < 15; i++)
> +            tracker.log(null, null);
> +
> +        tracker.removedService(mockSvcRef, mockRequestLog);
> +
> +        // And these will not.
> +        for (int i = 0; i < 50; i++)
> +            tracker.log(null, null);
> +
> +        verify(mockRequestLog, times(15)).log(isNull(Request.class), isNull(Response.class));
> +    }
> +
> +    @Test
> +    public void testNaughtyService() throws Exception {
> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
> +
> +        AtomicInteger counter = new AtomicInteger(0);
> +        RequestLog mockRequestLog = new RequestLog() {
> +            @Override
> +            public void log(Request request, Response response) {
> +                counter.addAndGet(1);
> +                throw new RuntimeException("This service always explodes");
> +            }
> +        };
> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
> +        Bundle mockBundle = mock(Bundle.class);
> +        when(mockSvcRef.getBundle()).thenReturn(mockBundle);
> +        when(mockBundle.getSymbolicName()).thenReturn("org.example");
> +        when(mockBundle.getVersion()).thenReturn(new Version("1.0.0"));
> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
> +
> +        tracker.addingService(mockSvcRef);
> +
> +        // Invoke 200 times
> +        for (int i = 0; i < 200; i++)
> +            tracker.log(null, null);
> +
> +        tracker.removedService(mockSvcRef, mockRequestLog);
> +
> +        // Invoked 100 times and then removed
> +        assertEquals(100, counter.get());
> +    }
> +}
> 
> 
-- 
Carsten Ziegeler
Adobe Research Switzerland
cziegeler@apache.org

Re: svn commit: r1815249 - in /felix/trunk/http/jetty: ./ src/main/java/org/apache/felix/http/jetty/internal/ src/test/java/org/apache/felix/http/jetty/internal/

Posted by Neil Bartlett <nj...@gmail.com>.
Done, and done: https://issues.apache.org/jira/browse/FELIX-5744 <https://issues.apache.org/jira/browse/FELIX-5744>

Thanks guys.

> On 14 Nov 2017, at 19:52, Karl Pauls <ka...@gmail.com> wrote:
> 
> On Tue, Nov 14, 2017 at 8:51 PM, Neil Bartlett (Paremus)
> <neil.bartlett@paremus.com <ma...@paremus.com>> wrote:
>> 
>>> On 14 Nov 2017, at 19:32, Carsten Ziegeler <cziegeler@apache.org <ma...@apache.org>> wrote:
>>> 
>>> Hi,
>>> 
>>> do we have a Jira issue covering this?
>> 
>> No this didn’t come from a JIRA.
> 
> Could you maybe create one so that we don't forget that this happend?
> 
> regards,
> 
> Karl
> 
>>> 
>>> Could you please also apply the patch at the R7 branch so they don't get
>>> out of sync?
>> 
>> Sure, will do.
>> 
>> Neil
>> 
>>> 
>>> Thanks
>>> 
>>> Carsten
>>> 
>>> 
>>> Nbartlett wrote
>>>> Author: nbartlett
>>>> Date: Tue Nov 14 19:09:34 2017
>>>> New Revision: 1815249
>>>> 
>>>> URL: http://svn.apache.org/viewvc?rev=1815249&view=rev
>>>> Log:
>>>> Implement HTTP request logging. This closes #127
>>>> 
>>>> Added:
>>>>   felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
>>>>   felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
>>>>   felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
>>>>   felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
>>>> Modified:
>>>>   felix/trunk/http/jetty/pom.xml
>>>>   felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
>>>>   felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
>>>> 
>>>> Modified: felix/trunk/http/jetty/pom.xml
>>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/pom.xml?rev=1815249&r1=1815248&r2=1815249&view=diff
>>>> ==============================================================================
>>>> --- felix/trunk/http/jetty/pom.xml (original)
>>>> +++ felix/trunk/http/jetty/pom.xml Tue Nov 14 19:09:34 2017
>>>> @@ -38,6 +38,8 @@
>>>>    </scm>
>>>> 
>>>>    <properties>
>>>> +        <!-- Skip because of problems with Java 8 -->
>>>> +        <animal.sniffer.skip>true</animal.sniffer.skip>
>>>>        <felix.java.version>8</felix.java.version>
>>>>        <jetty.version>9.3.22.v20171030</jetty.version>
>>>>    </properties>
>>>> 
>>>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
>>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java?rev=1815249&view=auto
>>>> ==============================================================================
>>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java (added)
>>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java Tue Nov 14 19:09:34 2017
>>>> @@ -0,0 +1,97 @@
>>>> +/*
>>>> + * 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.felix.http.jetty.internal;
>>>> +
>>>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>>>> +import org.eclipse.jetty.server.*;
>>>> +import org.osgi.framework.BundleContext;
>>>> +import org.osgi.framework.ServiceRegistration;
>>>> +
>>>> +import java.io.File;
>>>> +import java.io.IOException;
>>>> +import java.nio.file.Files;
>>>> +import java.nio.file.attribute.PosixFilePermission;
>>>> +import java.nio.file.attribute.PosixFilePermissions;
>>>> +import java.util.Dictionary;
>>>> +import java.util.Hashtable;
>>>> +
>>>> +class FileRequestLog {
>>>> +
>>>> +    public static final String SVC_PROP_NAME = "name";
>>>> +    public static final String DEFAULT_NAME = "file";
>>>> +    public static final String SVC_PROP_FILEPATH = "filepath";
>>>> +
>>>> +    private final NCSARequestLog delegate;
>>>> +    private final String logFilePath;
>>>> +    private final String serviceName;
>>>> +    private ServiceRegistration<RequestLog> registration = null;
>>>> +
>>>> +    FileRequestLog(JettyConfig config) {
>>>> +        logFilePath = config.getRequestLogFilePath();
>>>> +        serviceName = config.getRequestLogFileServiceName() != null ? config.getRequestLogFileServiceName() : DEFAULT_NAME;
>>>> +        if (config.isRequestLogFileAsync()) {
>>>> +            delegate = new AsyncNCSARequestLog(logFilePath);
>>>> +        } else {
>>>> +            delegate = new NCSARequestLog(logFilePath);
>>>> +        }
>>>> +
>>>> +        delegate.setAppend(config.isRequestLogFileAppend());
>>>> +        delegate.setRetainDays(config.getRequestLogFileRetainDays());
>>>> +        delegate.setFilenameDateFormat(config.getRequestLogFilenameDateFormat());
>>>> +        delegate.setExtended(config.isRequestLogFileExtended());
>>>> +        delegate.setIgnorePaths(config.getRequestLogFileIgnorePaths());
>>>> +        delegate.setLogCookies(config.isRequestLogFileLogCookies());
>>>> +        delegate.setLogServer(config.isRequestLogFileLogServer());
>>>> +        delegate.setLogLatency(config.isRequestLogFileLogLatency());
>>>> +    }
>>>> +
>>>> +    synchronized void start(BundleContext context) throws IOException, IllegalStateException {
>>>> +        File logFile = new File(logFilePath).getAbsoluteFile();
>>>> +        File logFileDir = logFile.getParentFile();
>>>> +        if (logFileDir != null && !logFileDir.isDirectory()) {
>>>> +            SystemLogger.info("Creating directory " + logFileDir.getAbsolutePath());
>>>> +            Files.createDirectories(logFileDir.toPath(), PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
>>>> +        }
>>>> +
>>>> +        if (registration != null) {
>>>> +            throw new IllegalStateException(getClass().getSimpleName() + " is already started");
>>>> +        }
>>>> +        try {
>>>> +            delegate.start();
>>>> +            Dictionary<String, Object> svcProps = new Hashtable<>();
>>>> +            svcProps.put(SVC_PROP_NAME, serviceName);
>>>> +            svcProps.put(SVC_PROP_FILEPATH, logFilePath);
>>>> +            registration = context.registerService(RequestLog.class, delegate, svcProps);
>>>> +        } catch (Exception e) {
>>>> +            SystemLogger.error("Error starting File Request Log", e);
>>>> +        }
>>>> +    }
>>>> +
>>>> +    synchronized void stop() {
>>>> +        try {
>>>> +            if (registration != null) {
>>>> +                registration.unregister();
>>>> +            }
>>>> +            delegate.stop();;
>>>> +        } catch (Exception e) {
>>>> +            SystemLogger.error("Error shutting down File Request Log", e);
>>>> +        } finally {
>>>> +            registration = null;
>>>> +        }
>>>> +    }
>>>> +
>>>> +}
>>>> 
>>>> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
>>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java?rev=1815249&r1=1815248&r2=1815249&view=diff
>>>> ==============================================================================
>>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java (original)
>>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java Tue Nov 14 19:09:34 2017
>>>> @@ -165,6 +165,51 @@ public final class JettyConfig
>>>>    /** Felix specific property to set HTTP instance name. */
>>>>    public static final String FELIX_HTTP_SERVICE_NAME = "org.apache.felix.http.name";
>>>> 
>>>> +    /** Felix specific property to configure a filter for RequestLog services */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILTER = "org.apache.felix.http.requestlog.filter";
>>>> +
>>>> +    /** Felix specific property to enable request logging to the OSGi Log Service */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE = "org.apache.felix.http.requestlog.osgi.enable";
>>>> +
>>>> +    /** Felix specific property to specify the published "name" property of the OSGi Log Service-base Request Log service. Allows server configs to filter on specific log services. */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME = "org.apache.felix.http.requestlog.osgi.name";
>>>> +
>>>> +    /** Felix specific property to control the level of the log messages generated by the OSGi Log Service-based request log. Values must correspond to the constants defined in the LogService interface, default is 3 "INFO". */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL = "org.apache.felix.http.requestlog.osgi.level";
>>>> +
>>>> +    /** Felix specific property to enable request logging to a file and provide the path to that file. Default is null meaning that the file log is disabled. */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_PATH = "org.apache.felix.http.requestlog.file.path";
>>>> +
>>>> +    /** Felix specific property to specify the published "name" property of the file-based RequestLog service. Allows server configs to filter on specific log services. */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME = "org.apache.felix.http.requestlog.file.name";
>>>> +
>>>> +    /** Felix specific property to enable file request logging to be asynchronous */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_ASYNC = "org.apache.felix.http.requestlog.file.async";
>>>> +
>>>> +    /** Felix specific property to enable request logging to append to the log file rather than overwriting */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_APPEND = "org.apache.felix.http.requestlog.file.append";
>>>> +
>>>> +    /** Felix specific property to specify the number of days the request log file is retained */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS = "org.apache.felix.http.requestlog.file.retaindays";
>>>> +
>>>> +    /** Felix specific property to specify the date format in request log file names */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT = "org.apache.felix.http.requestlog.file.dateformat";
>>>> +
>>>> +    /** Felix specific property to enable extended request logging to a named file */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED = "org.apache.felix.http.requestlog.file.extended";
>>>> +
>>>> +    /** Felix specific property to ignore matching paths in the request log file */
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS = "org.apache.felix.http.requestlog.file.ignorepaths";
>>>> +
>>>> +    /** Felix specific property to enable request logging cookies in the request log file*/
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES = "org.apache.felix.http.requestlog.file.logcookies";
>>>> +
>>>> +    /** Felix specific property to enable request logging the host name in the request log file*/
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER = "org.apache.felix.http.requestlog.file.logserver";
>>>> +
>>>> +    /** Felix specific property to enable request logging request processing time in the request log file*/
>>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY = "org.apache.felix.http.requestlog.file.loglatency";
>>>> +
>>>>    /** Felix specific property to define custom properties for the http runtime service. */
>>>>    public static final String FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX = "org.apache.felix.http.runtime.init.";
>>>> 
>>>> @@ -425,6 +470,66 @@ public final class JettyConfig
>>>>     return (String) getProperty(FELIX_HTTP_SERVICE_NAME);
>>>>    }
>>>> 
>>>> +    public String getRequestLogFilter() {
>>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILTER, null);
>>>> +    }
>>>> +
>>>> +    public boolean isRequestLogOSGiEnabled() {
>>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE, false);
>>>> +    }
>>>> +
>>>> +    public String getRequestLogOSGiServiceName() {
>>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME);
>>>> +    }
>>>> +
>>>> +    public int getRequestLogOSGiLevel() {
>>>> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL, 3); // 3 == LogService.LOG_INFO
>>>> +    }
>>>> +
>>>> +    public String getRequestLogFilePath() {
>>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_PATH, null);
>>>> +    }
>>>> +
>>>> +    public String getRequestLogFileServiceName() {
>>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME, "file");
>>>> +    }
>>>> +
>>>> +    public boolean isRequestLogFileAsync() {
>>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_ASYNC, false);
>>>> +    }
>>>> +
>>>> +    public boolean isRequestLogFileAppend() {
>>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_APPEND, true);
>>>> +    }
>>>> +
>>>> +    public int getRequestLogFileRetainDays() {
>>>> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS, 31);
>>>> +    }
>>>> +
>>>> +    public String getRequestLogFilenameDateFormat() {
>>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT, null);
>>>> +    }
>>>> +
>>>> +    public boolean isRequestLogFileExtended() {
>>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED, false);
>>>> +    }
>>>> +
>>>> +    public String[] getRequestLogFileIgnorePaths() {
>>>> +        return getStringArrayProperty(FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS, new String[0]);
>>>> +    }
>>>> +
>>>> +    public boolean isRequestLogFileLogCookies() {
>>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES, false);
>>>> +    }
>>>> +
>>>> +    public boolean isRequestLogFileLogServer() {
>>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER, false);
>>>> +    }
>>>> +
>>>> +    public boolean isRequestLogFileLogLatency() {
>>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY, false);
>>>> +    }
>>>> +
>>>>    public void reset()
>>>>    {
>>>>        update(null);
>>>> @@ -668,4 +773,5 @@ public final class JettyConfig
>>>>            return dflt;
>>>>        }
>>>>    }
>>>> +
>>>> }
>>>> 
>>>> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
>>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java?rev=1815249&r1=1815248&r2=1815249&view=diff
>>>> ==============================================================================
>>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java (original)
>>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java Tue Nov 14 19:09:34 2017
>>>> @@ -101,11 +101,15 @@ public final class JettyService extends
>>>>    private volatile BundleTracker<Deployment> bundleTracker;
>>>>    private volatile ServiceTracker<EventAdmin, EventAdmin> eventAdmintTracker;
>>>>    private volatile ConnectorFactoryTracker connectorTracker;
>>>> +    private volatile RequestLogTracker requestLogTracker;
>>>> +    private volatile LogServiceRequestLog osgiRequestLog;
>>>> +    private volatile FileRequestLog fileRequestLog;
>>>>    private volatile LoadBalancerCustomizerFactoryTracker loadBalancerCustomizerTracker;
>>>>    private volatile CustomizerWrapper customizerWrapper;
>>>>    private volatile EventAdmin eventAdmin;
>>>>    private boolean registerManagedService = true;
>>>> 
>>>> +
>>>>    public JettyService(final BundleContext context,
>>>>            final HttpServiceController controller)
>>>>    {
>>>> @@ -292,6 +296,22 @@ public final class JettyService extends
>>>>            this.controller.getEventDispatcher().setActive(false);
>>>>            this.controller.unregister();
>>>> 
>>>> +            if (this.fileRequestLog != null)
>>>> +            {
>>>> +                this.fileRequestLog.stop();
>>>> +                this.fileRequestLog = null;
>>>> +            }
>>>> +            if (this.osgiRequestLog != null)
>>>> +            {
>>>> +                this.osgiRequestLog.unregister();
>>>> +                this.osgiRequestLog = null;
>>>> +            }
>>>> +            if (this.requestLogTracker != null)
>>>> +            {
>>>> +                this.requestLogTracker.close();
>>>> +                this.requestLogTracker = null;
>>>> +            }
>>>> +
>>>>            if (this.connectorTracker != null)
>>>>            {
>>>>                this.connectorTracker.close();
>>>> @@ -413,6 +433,26 @@ public final class JettyService extends
>>>>                this.stopJetty();
>>>>                SystemLogger.error("Jetty stopped (no connectors available)", null);
>>>>            }
>>>> +
>>>> +            try {
>>>> +                this.requestLogTracker = new RequestLogTracker(this.context, this.config.getRequestLogFilter());
>>>> +                this.requestLogTracker.open();
>>>> +                this.server.setRequestLog(requestLogTracker);
>>>> +            } catch (InvalidSyntaxException e) {
>>>> +                SystemLogger.error("Invalid filter syntax in request log tracker", e);
>>>> +            }
>>>> +
>>>> +            if (this.config.isRequestLogOSGiEnabled()) {
>>>> +                this.osgiRequestLog = new LogServiceRequestLog(this.config);
>>>> +                this.osgiRequestLog.register(this.context);
>>>> +                SystemLogger.info("Directing Jetty request logs to the OSGi Log Service");
>>>> +            }
>>>> +
>>>> +            if (this.config.getRequestLogFilePath() != null && !this.config.getRequestLogFilePath().isEmpty()) {
>>>> +                this.fileRequestLog = new FileRequestLog(config);
>>>> +                this.fileRequestLog.start(this.context);
>>>> +                SystemLogger.info("Directing Jetty request logs to " + this.config.getRequestLogFilePath());
>>>> +            }
>>>>        }
>>>>        else
>>>>        {
>>>> 
>>>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
>>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java?rev=1815249&view=auto
>>>> ==============================================================================
>>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java (added)
>>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java Tue Nov 14 19:09:34 2017
>>>> @@ -0,0 +1,80 @@
>>>> +/*
>>>> + * 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.felix.http.jetty.internal;
>>>> +
>>>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>>>> +import org.eclipse.jetty.server.AbstractNCSARequestLog;
>>>> +import org.eclipse.jetty.server.RequestLog;
>>>> +import org.osgi.framework.BundleContext;
>>>> +import org.osgi.framework.ServiceRegistration;
>>>> +
>>>> +import java.io.IOException;
>>>> +import java.util.Dictionary;
>>>> +import java.util.Hashtable;
>>>> +import java.util.concurrent.atomic.AtomicReference;
>>>> +
>>>> +/**
>>>> + * A RequestLog that logs to the OSGi LogService when present. Not registered by default.
>>>> + */
>>>> +class LogServiceRequestLog extends AbstractNCSARequestLog {
>>>> +
>>>> +    public static final String SVC_PROP_NAME = "name";
>>>> +    public static final String DEFAULT_NAME = "osgi";
>>>> +    public static final String PREFIX = "REQUEST: ";
>>>> +
>>>> +    private static final int DEFAULT_LOG_LEVEL = 3; // LogService.LOG_INFO
>>>> +
>>>> +    private final int logLevel;
>>>> +    private final String serviceName;
>>>> +
>>>> +    private ServiceRegistration<RequestLog> registration;
>>>> +
>>>> +    LogServiceRequestLog(JettyConfig config) {
>>>> +        this.serviceName = config.getRequestLogOSGiServiceName();
>>>> +        this.logLevel = config.getRequestLogOSGiLevel();
>>>> +    }
>>>> +
>>>> +    public synchronized void register(BundleContext context) throws IllegalStateException {
>>>> +        if (registration != null) {
>>>> +            throw new IllegalStateException(getClass().getSimpleName() + " already registered");
>>>> +        }
>>>> +        Dictionary<String, Object> svcProps = new Hashtable<>();
>>>> +        svcProps.put(SVC_PROP_NAME, serviceName);
>>>> +        this.registration = context.registerService(RequestLog.class, this, svcProps);
>>>> +    }
>>>> +
>>>> +    public synchronized void unregister() {
>>>> +        try {
>>>> +            if (registration != null) {
>>>> +                registration.unregister();;
>>>> +            }
>>>> +        } finally {
>>>> +            registration = null;
>>>> +        }
>>>> +    }
>>>> +
>>>> +    @Override
>>>> +    public void write(String s) throws IOException {
>>>> +        SystemLogger.info(PREFIX + s);
>>>> +    }
>>>> +
>>>> +    @Override
>>>> +    protected boolean isEnabled() {
>>>> +        return true;
>>>> +    }
>>>> +
>>>> +}
>>>> 
>>>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
>>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java?rev=1815249&view=auto
>>>> ==============================================================================
>>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java (added)
>>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java Tue Nov 14 19:09:34 2017
>>>> @@ -0,0 +1,106 @@
>>>> +/*
>>>> + * 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.felix.http.jetty.internal;
>>>> +
>>>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>>>> +import org.eclipse.jetty.server.Request;
>>>> +import org.eclipse.jetty.server.RequestLog;
>>>> +import org.eclipse.jetty.server.Response;
>>>> +import org.osgi.framework.*;
>>>> +import org.osgi.util.tracker.ServiceTracker;
>>>> +
>>>> +import java.util.Map;
>>>> +import java.util.concurrent.ConcurrentHashMap;
>>>> +import java.util.concurrent.ConcurrentMap;
>>>> +
>>>> +/**
>>>> + * An instance of Jetty's RequestLog that dispatches to registered RequestLog services in the service registry. A filter
>>>> + * can be provided so that it only dispatches to selected services.
>>>> + * <p>
>>>> + * Unchecked exceptions from the RequestLog services are caught and logged to the OSGi LogService. to avoid flooding the
>>>> + * LogService, we will remove a RequestLog service if it breaches a maximum number of errors (see {@link
>>>> + * RequestLogTracker#MAX_ERROR_COUNT}). Once this happens we will stop dispatching to that service entirely until it is
>>>> + * unregistered.
>>>> + */
>>>> +class RequestLogTracker extends ServiceTracker<RequestLog, RequestLog>  implements RequestLog {
>>>> +
>>>> +    private static final int MAX_ERROR_COUNT = 100;
>>>> +
>>>> +    private final ConcurrentMap<ServiceReference<?>, RequestLog> logSvcs = new ConcurrentHashMap<>();
>>>> +    private final ConcurrentMap<ServiceReference<?>, Integer> naughtyStep = new ConcurrentHashMap<>();
>>>> +
>>>> +    RequestLogTracker(BundleContext context, String filter) throws InvalidSyntaxException {
>>>> +        super(context, buildFilter(filter), null);
>>>> +    }
>>>> +
>>>> +    private static Filter buildFilter(String inputFilter) throws InvalidSyntaxException {
>>>> +        String objectClassFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, RequestLog.class.getName());
>>>> +        String compositeFilter;
>>>> +        if (inputFilter != null) {
>>>> +            // Parse the input filter just for validation before we insert into ours.
>>>> +            FrameworkUtil.createFilter(inputFilter);
>>>> +            compositeFilter = "(&" + objectClassFilter + inputFilter + ")";
>>>> +        } else {
>>>> +            compositeFilter = objectClassFilter;
>>>> +        }
>>>> +        return FrameworkUtil.createFilter(compositeFilter);
>>>> +    }
>>>> +
>>>> +    @Override
>>>> +    public RequestLog addingService(ServiceReference<RequestLog> reference) {
>>>> +        RequestLog logSvc = context.getService(reference);
>>>> +        logSvcs.put(reference, logSvc);
>>>> +        return logSvc;
>>>> +    }
>>>> +
>>>> +    @Override
>>>> +    public void removedService(ServiceReference<RequestLog> reference, RequestLog logSvc) {
>>>> +        logSvcs.remove(reference);
>>>> +        naughtyStep.remove(reference);
>>>> +        context.ungetService(reference);
>>>> +    }
>>>> +
>>>> +    @Override
>>>> +    public void log(Request request, Response response) {
>>>> +        for (Map.Entry<ServiceReference<?>, RequestLog> entry : logSvcs.entrySet()) {
>>>> +            try {
>>>> +                entry.getValue().log(request, response);
>>>> +            } catch (Exception e) {
>>>> +                processError(entry.getKey(), e);
>>>> +            }
>>>> +        }
>>>> +    }
>>>> +
>>>> +    /**
>>>> +     * Process an exception from a RequestLog service instance, and remove the service if it has reached the maximum
>>>> +     * error limit.
>>>> +     */
>>>> +    private void processError(ServiceReference<?> reference, Exception e) {
>>>> +        SystemLogger.error(reference, String.format("Error dispatching to request log service ID %d from bundle %s:%s",
>>>> +                reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), e);
>>>> +
>>>> +        int naughty = naughtyStep.merge(reference, 1, Integer::sum);
>>>> +        if (naughty >= MAX_ERROR_COUNT) {
>>>> +            // We have reached (but not exceeded) the maximum, and the last error has been logged. Remove from the maps
>>>> +            // so we will not invoke the service again.
>>>> +            logSvcs.remove(reference);
>>>> +            naughtyStep.remove(reference);
>>>> +            SystemLogger.error(reference, String.format("RequestLog service ID %d from bundle %s:%s threw too many errors, it will no longer be invoked.",
>>>> +                    reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), null);
>>>> +        }
>>>> +    }
>>>> +}
>>>> 
>>>> Added: felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
>>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java?rev=1815249&view=auto
>>>> ==============================================================================
>>>> --- felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java (added)
>>>> +++ felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java Tue Nov 14 19:09:34 2017
>>>> @@ -0,0 +1,97 @@
>>>> +/*
>>>> + * 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.felix.http.jetty.internal;
>>>> +
>>>> +import org.eclipse.jetty.server.Request;
>>>> +import org.eclipse.jetty.server.RequestLog;
>>>> +import org.eclipse.jetty.server.Response;
>>>> +import org.junit.Test;
>>>> +import org.junit.runner.RunWith;
>>>> +import org.mockito.Mock;
>>>> +import org.mockito.runners.MockitoJUnitRunner;
>>>> +import org.osgi.framework.*;
>>>> +
>>>> +import java.util.concurrent.atomic.AtomicInteger;
>>>> +
>>>> +import static org.junit.Assert.assertEquals;
>>>> +import static org.mockito.Mockito.*;
>>>> +
>>>> +@RunWith(MockitoJUnitRunner.class)
>>>> +public class RequestLogTrackerTest {
>>>> +
>>>> +    @Mock
>>>> +    BundleContext context;
>>>> +
>>>> +    @Test
>>>> +    public void testInvokeRequestLog() throws Exception {
>>>> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
>>>> +
>>>> +        RequestLog mockRequestLog = mock(RequestLog.class);
>>>> +
>>>> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
>>>> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
>>>> +
>>>> +        // These invocations not passed through to the mock because it is not registered yet
>>>> +        for (int i = 0; i < 10; i++)
>>>> +            tracker.log(null, null);
>>>> +
>>>> +        tracker.addingService(mockSvcRef);
>>>> +
>>>> +        // These will pass through
>>>> +        for (int i = 0; i < 15; i++)
>>>> +            tracker.log(null, null);
>>>> +
>>>> +        tracker.removedService(mockSvcRef, mockRequestLog);
>>>> +
>>>> +        // And these will not.
>>>> +        for (int i = 0; i < 50; i++)
>>>> +            tracker.log(null, null);
>>>> +
>>>> +        verify(mockRequestLog, times(15)).log(isNull(Request.class), isNull(Response.class));
>>>> +    }
>>>> +
>>>> +    @Test
>>>> +    public void testNaughtyService() throws Exception {
>>>> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
>>>> +
>>>> +        AtomicInteger counter = new AtomicInteger(0);
>>>> +        RequestLog mockRequestLog = new RequestLog() {
>>>> +            @Override
>>>> +            public void log(Request request, Response response) {
>>>> +                counter.addAndGet(1);
>>>> +                throw new RuntimeException("This service always explodes");
>>>> +            }
>>>> +        };
>>>> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
>>>> +        Bundle mockBundle = mock(Bundle.class);
>>>> +        when(mockSvcRef.getBundle()).thenReturn(mockBundle);
>>>> +        when(mockBundle.getSymbolicName()).thenReturn("org.example");
>>>> +        when(mockBundle.getVersion()).thenReturn(new Version("1.0.0"));
>>>> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
>>>> +
>>>> +        tracker.addingService(mockSvcRef);
>>>> +
>>>> +        // Invoke 200 times
>>>> +        for (int i = 0; i < 200; i++)
>>>> +            tracker.log(null, null);
>>>> +
>>>> +        tracker.removedService(mockSvcRef, mockRequestLog);
>>>> +
>>>> +        // Invoked 100 times and then removed
>>>> +        assertEquals(100, counter.get());
>>>> +    }
>>>> +}
>>>> 
>>>> 
>>> --
>>> Carsten Ziegeler
>>> Adobe Research Switzerland
>>> cziegeler@apache.org
>> 
> 
> 
> 
> -- 
> Karl Pauls
> karlpauls@gmail.com <ma...@gmail.com>

Re: svn commit: r1815249 - in /felix/trunk/http/jetty: ./ src/main/java/org/apache/felix/http/jetty/internal/ src/test/java/org/apache/felix/http/jetty/internal/

Posted by Karl Pauls <ka...@gmail.com>.
On Tue, Nov 14, 2017 at 8:51 PM, Neil Bartlett (Paremus)
<ne...@paremus.com> wrote:
>
>> On 14 Nov 2017, at 19:32, Carsten Ziegeler <cz...@apache.org> wrote:
>>
>> Hi,
>>
>> do we have a Jira issue covering this?
>
> No this didn’t come from a JIRA.

Could you maybe create one so that we don't forget that this happend?

regards,

Karl

>>
>> Could you please also apply the patch at the R7 branch so they don't get
>> out of sync?
>
> Sure, will do.
>
> Neil
>
>>
>> Thanks
>>
>> Carsten
>>
>>
>> Nbartlett wrote
>>> Author: nbartlett
>>> Date: Tue Nov 14 19:09:34 2017
>>> New Revision: 1815249
>>>
>>> URL: http://svn.apache.org/viewvc?rev=1815249&view=rev
>>> Log:
>>> Implement HTTP request logging. This closes #127
>>>
>>> Added:
>>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
>>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
>>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
>>>    felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
>>> Modified:
>>>    felix/trunk/http/jetty/pom.xml
>>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
>>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
>>>
>>> Modified: felix/trunk/http/jetty/pom.xml
>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/pom.xml?rev=1815249&r1=1815248&r2=1815249&view=diff
>>> ==============================================================================
>>> --- felix/trunk/http/jetty/pom.xml (original)
>>> +++ felix/trunk/http/jetty/pom.xml Tue Nov 14 19:09:34 2017
>>> @@ -38,6 +38,8 @@
>>>     </scm>
>>>
>>>     <properties>
>>> +        <!-- Skip because of problems with Java 8 -->
>>> +        <animal.sniffer.skip>true</animal.sniffer.skip>
>>>         <felix.java.version>8</felix.java.version>
>>>         <jetty.version>9.3.22.v20171030</jetty.version>
>>>     </properties>
>>>
>>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java?rev=1815249&view=auto
>>> ==============================================================================
>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java (added)
>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java Tue Nov 14 19:09:34 2017
>>> @@ -0,0 +1,97 @@
>>> +/*
>>> + * 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.felix.http.jetty.internal;
>>> +
>>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>>> +import org.eclipse.jetty.server.*;
>>> +import org.osgi.framework.BundleContext;
>>> +import org.osgi.framework.ServiceRegistration;
>>> +
>>> +import java.io.File;
>>> +import java.io.IOException;
>>> +import java.nio.file.Files;
>>> +import java.nio.file.attribute.PosixFilePermission;
>>> +import java.nio.file.attribute.PosixFilePermissions;
>>> +import java.util.Dictionary;
>>> +import java.util.Hashtable;
>>> +
>>> +class FileRequestLog {
>>> +
>>> +    public static final String SVC_PROP_NAME = "name";
>>> +    public static final String DEFAULT_NAME = "file";
>>> +    public static final String SVC_PROP_FILEPATH = "filepath";
>>> +
>>> +    private final NCSARequestLog delegate;
>>> +    private final String logFilePath;
>>> +    private final String serviceName;
>>> +    private ServiceRegistration<RequestLog> registration = null;
>>> +
>>> +    FileRequestLog(JettyConfig config) {
>>> +        logFilePath = config.getRequestLogFilePath();
>>> +        serviceName = config.getRequestLogFileServiceName() != null ? config.getRequestLogFileServiceName() : DEFAULT_NAME;
>>> +        if (config.isRequestLogFileAsync()) {
>>> +            delegate = new AsyncNCSARequestLog(logFilePath);
>>> +        } else {
>>> +            delegate = new NCSARequestLog(logFilePath);
>>> +        }
>>> +
>>> +        delegate.setAppend(config.isRequestLogFileAppend());
>>> +        delegate.setRetainDays(config.getRequestLogFileRetainDays());
>>> +        delegate.setFilenameDateFormat(config.getRequestLogFilenameDateFormat());
>>> +        delegate.setExtended(config.isRequestLogFileExtended());
>>> +        delegate.setIgnorePaths(config.getRequestLogFileIgnorePaths());
>>> +        delegate.setLogCookies(config.isRequestLogFileLogCookies());
>>> +        delegate.setLogServer(config.isRequestLogFileLogServer());
>>> +        delegate.setLogLatency(config.isRequestLogFileLogLatency());
>>> +    }
>>> +
>>> +    synchronized void start(BundleContext context) throws IOException, IllegalStateException {
>>> +        File logFile = new File(logFilePath).getAbsoluteFile();
>>> +        File logFileDir = logFile.getParentFile();
>>> +        if (logFileDir != null && !logFileDir.isDirectory()) {
>>> +            SystemLogger.info("Creating directory " + logFileDir.getAbsolutePath());
>>> +            Files.createDirectories(logFileDir.toPath(), PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
>>> +        }
>>> +
>>> +        if (registration != null) {
>>> +            throw new IllegalStateException(getClass().getSimpleName() + " is already started");
>>> +        }
>>> +        try {
>>> +            delegate.start();
>>> +            Dictionary<String, Object> svcProps = new Hashtable<>();
>>> +            svcProps.put(SVC_PROP_NAME, serviceName);
>>> +            svcProps.put(SVC_PROP_FILEPATH, logFilePath);
>>> +            registration = context.registerService(RequestLog.class, delegate, svcProps);
>>> +        } catch (Exception e) {
>>> +            SystemLogger.error("Error starting File Request Log", e);
>>> +        }
>>> +    }
>>> +
>>> +    synchronized void stop() {
>>> +        try {
>>> +            if (registration != null) {
>>> +                registration.unregister();
>>> +            }
>>> +            delegate.stop();;
>>> +        } catch (Exception e) {
>>> +            SystemLogger.error("Error shutting down File Request Log", e);
>>> +        } finally {
>>> +            registration = null;
>>> +        }
>>> +    }
>>> +
>>> +}
>>>
>>> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java?rev=1815249&r1=1815248&r2=1815249&view=diff
>>> ==============================================================================
>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java (original)
>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java Tue Nov 14 19:09:34 2017
>>> @@ -165,6 +165,51 @@ public final class JettyConfig
>>>     /** Felix specific property to set HTTP instance name. */
>>>     public static final String FELIX_HTTP_SERVICE_NAME = "org.apache.felix.http.name";
>>>
>>> +    /** Felix specific property to configure a filter for RequestLog services */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILTER = "org.apache.felix.http.requestlog.filter";
>>> +
>>> +    /** Felix specific property to enable request logging to the OSGi Log Service */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE = "org.apache.felix.http.requestlog.osgi.enable";
>>> +
>>> +    /** Felix specific property to specify the published "name" property of the OSGi Log Service-base Request Log service. Allows server configs to filter on specific log services. */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME = "org.apache.felix.http.requestlog.osgi.name";
>>> +
>>> +    /** Felix specific property to control the level of the log messages generated by the OSGi Log Service-based request log. Values must correspond to the constants defined in the LogService interface, default is 3 "INFO". */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL = "org.apache.felix.http.requestlog.osgi.level";
>>> +
>>> +    /** Felix specific property to enable request logging to a file and provide the path to that file. Default is null meaning that the file log is disabled. */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_PATH = "org.apache.felix.http.requestlog.file.path";
>>> +
>>> +    /** Felix specific property to specify the published "name" property of the file-based RequestLog service. Allows server configs to filter on specific log services. */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME = "org.apache.felix.http.requestlog.file.name";
>>> +
>>> +    /** Felix specific property to enable file request logging to be asynchronous */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_ASYNC = "org.apache.felix.http.requestlog.file.async";
>>> +
>>> +    /** Felix specific property to enable request logging to append to the log file rather than overwriting */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_APPEND = "org.apache.felix.http.requestlog.file.append";
>>> +
>>> +    /** Felix specific property to specify the number of days the request log file is retained */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS = "org.apache.felix.http.requestlog.file.retaindays";
>>> +
>>> +    /** Felix specific property to specify the date format in request log file names */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT = "org.apache.felix.http.requestlog.file.dateformat";
>>> +
>>> +    /** Felix specific property to enable extended request logging to a named file */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED = "org.apache.felix.http.requestlog.file.extended";
>>> +
>>> +    /** Felix specific property to ignore matching paths in the request log file */
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS = "org.apache.felix.http.requestlog.file.ignorepaths";
>>> +
>>> +    /** Felix specific property to enable request logging cookies in the request log file*/
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES = "org.apache.felix.http.requestlog.file.logcookies";
>>> +
>>> +    /** Felix specific property to enable request logging the host name in the request log file*/
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER = "org.apache.felix.http.requestlog.file.logserver";
>>> +
>>> +    /** Felix specific property to enable request logging request processing time in the request log file*/
>>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY = "org.apache.felix.http.requestlog.file.loglatency";
>>> +
>>>     /** Felix specific property to define custom properties for the http runtime service. */
>>>     public static final String FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX = "org.apache.felix.http.runtime.init.";
>>>
>>> @@ -425,6 +470,66 @@ public final class JettyConfig
>>>      return (String) getProperty(FELIX_HTTP_SERVICE_NAME);
>>>     }
>>>
>>> +    public String getRequestLogFilter() {
>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILTER, null);
>>> +    }
>>> +
>>> +    public boolean isRequestLogOSGiEnabled() {
>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE, false);
>>> +    }
>>> +
>>> +    public String getRequestLogOSGiServiceName() {
>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME);
>>> +    }
>>> +
>>> +    public int getRequestLogOSGiLevel() {
>>> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL, 3); // 3 == LogService.LOG_INFO
>>> +    }
>>> +
>>> +    public String getRequestLogFilePath() {
>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_PATH, null);
>>> +    }
>>> +
>>> +    public String getRequestLogFileServiceName() {
>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME, "file");
>>> +    }
>>> +
>>> +    public boolean isRequestLogFileAsync() {
>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_ASYNC, false);
>>> +    }
>>> +
>>> +    public boolean isRequestLogFileAppend() {
>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_APPEND, true);
>>> +    }
>>> +
>>> +    public int getRequestLogFileRetainDays() {
>>> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS, 31);
>>> +    }
>>> +
>>> +    public String getRequestLogFilenameDateFormat() {
>>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT, null);
>>> +    }
>>> +
>>> +    public boolean isRequestLogFileExtended() {
>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED, false);
>>> +    }
>>> +
>>> +    public String[] getRequestLogFileIgnorePaths() {
>>> +        return getStringArrayProperty(FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS, new String[0]);
>>> +    }
>>> +
>>> +    public boolean isRequestLogFileLogCookies() {
>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES, false);
>>> +    }
>>> +
>>> +    public boolean isRequestLogFileLogServer() {
>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER, false);
>>> +    }
>>> +
>>> +    public boolean isRequestLogFileLogLatency() {
>>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY, false);
>>> +    }
>>> +
>>>     public void reset()
>>>     {
>>>         update(null);
>>> @@ -668,4 +773,5 @@ public final class JettyConfig
>>>             return dflt;
>>>         }
>>>     }
>>> +
>>> }
>>>
>>> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java?rev=1815249&r1=1815248&r2=1815249&view=diff
>>> ==============================================================================
>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java (original)
>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java Tue Nov 14 19:09:34 2017
>>> @@ -101,11 +101,15 @@ public final class JettyService extends
>>>     private volatile BundleTracker<Deployment> bundleTracker;
>>>     private volatile ServiceTracker<EventAdmin, EventAdmin> eventAdmintTracker;
>>>     private volatile ConnectorFactoryTracker connectorTracker;
>>> +    private volatile RequestLogTracker requestLogTracker;
>>> +    private volatile LogServiceRequestLog osgiRequestLog;
>>> +    private volatile FileRequestLog fileRequestLog;
>>>     private volatile LoadBalancerCustomizerFactoryTracker loadBalancerCustomizerTracker;
>>>     private volatile CustomizerWrapper customizerWrapper;
>>>     private volatile EventAdmin eventAdmin;
>>>     private boolean registerManagedService = true;
>>>
>>> +
>>>     public JettyService(final BundleContext context,
>>>             final HttpServiceController controller)
>>>     {
>>> @@ -292,6 +296,22 @@ public final class JettyService extends
>>>             this.controller.getEventDispatcher().setActive(false);
>>>             this.controller.unregister();
>>>
>>> +            if (this.fileRequestLog != null)
>>> +            {
>>> +                this.fileRequestLog.stop();
>>> +                this.fileRequestLog = null;
>>> +            }
>>> +            if (this.osgiRequestLog != null)
>>> +            {
>>> +                this.osgiRequestLog.unregister();
>>> +                this.osgiRequestLog = null;
>>> +            }
>>> +            if (this.requestLogTracker != null)
>>> +            {
>>> +                this.requestLogTracker.close();
>>> +                this.requestLogTracker = null;
>>> +            }
>>> +
>>>             if (this.connectorTracker != null)
>>>             {
>>>                 this.connectorTracker.close();
>>> @@ -413,6 +433,26 @@ public final class JettyService extends
>>>                 this.stopJetty();
>>>                 SystemLogger.error("Jetty stopped (no connectors available)", null);
>>>             }
>>> +
>>> +            try {
>>> +                this.requestLogTracker = new RequestLogTracker(this.context, this.config.getRequestLogFilter());
>>> +                this.requestLogTracker.open();
>>> +                this.server.setRequestLog(requestLogTracker);
>>> +            } catch (InvalidSyntaxException e) {
>>> +                SystemLogger.error("Invalid filter syntax in request log tracker", e);
>>> +            }
>>> +
>>> +            if (this.config.isRequestLogOSGiEnabled()) {
>>> +                this.osgiRequestLog = new LogServiceRequestLog(this.config);
>>> +                this.osgiRequestLog.register(this.context);
>>> +                SystemLogger.info("Directing Jetty request logs to the OSGi Log Service");
>>> +            }
>>> +
>>> +            if (this.config.getRequestLogFilePath() != null && !this.config.getRequestLogFilePath().isEmpty()) {
>>> +                this.fileRequestLog = new FileRequestLog(config);
>>> +                this.fileRequestLog.start(this.context);
>>> +                SystemLogger.info("Directing Jetty request logs to " + this.config.getRequestLogFilePath());
>>> +            }
>>>         }
>>>         else
>>>         {
>>>
>>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java?rev=1815249&view=auto
>>> ==============================================================================
>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java (added)
>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java Tue Nov 14 19:09:34 2017
>>> @@ -0,0 +1,80 @@
>>> +/*
>>> + * 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.felix.http.jetty.internal;
>>> +
>>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>>> +import org.eclipse.jetty.server.AbstractNCSARequestLog;
>>> +import org.eclipse.jetty.server.RequestLog;
>>> +import org.osgi.framework.BundleContext;
>>> +import org.osgi.framework.ServiceRegistration;
>>> +
>>> +import java.io.IOException;
>>> +import java.util.Dictionary;
>>> +import java.util.Hashtable;
>>> +import java.util.concurrent.atomic.AtomicReference;
>>> +
>>> +/**
>>> + * A RequestLog that logs to the OSGi LogService when present. Not registered by default.
>>> + */
>>> +class LogServiceRequestLog extends AbstractNCSARequestLog {
>>> +
>>> +    public static final String SVC_PROP_NAME = "name";
>>> +    public static final String DEFAULT_NAME = "osgi";
>>> +    public static final String PREFIX = "REQUEST: ";
>>> +
>>> +    private static final int DEFAULT_LOG_LEVEL = 3; // LogService.LOG_INFO
>>> +
>>> +    private final int logLevel;
>>> +    private final String serviceName;
>>> +
>>> +    private ServiceRegistration<RequestLog> registration;
>>> +
>>> +    LogServiceRequestLog(JettyConfig config) {
>>> +        this.serviceName = config.getRequestLogOSGiServiceName();
>>> +        this.logLevel = config.getRequestLogOSGiLevel();
>>> +    }
>>> +
>>> +    public synchronized void register(BundleContext context) throws IllegalStateException {
>>> +        if (registration != null) {
>>> +            throw new IllegalStateException(getClass().getSimpleName() + " already registered");
>>> +        }
>>> +        Dictionary<String, Object> svcProps = new Hashtable<>();
>>> +        svcProps.put(SVC_PROP_NAME, serviceName);
>>> +        this.registration = context.registerService(RequestLog.class, this, svcProps);
>>> +    }
>>> +
>>> +    public synchronized void unregister() {
>>> +        try {
>>> +            if (registration != null) {
>>> +                registration.unregister();;
>>> +            }
>>> +        } finally {
>>> +            registration = null;
>>> +        }
>>> +    }
>>> +
>>> +    @Override
>>> +    public void write(String s) throws IOException {
>>> +        SystemLogger.info(PREFIX + s);
>>> +    }
>>> +
>>> +    @Override
>>> +    protected boolean isEnabled() {
>>> +        return true;
>>> +    }
>>> +
>>> +}
>>>
>>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java?rev=1815249&view=auto
>>> ==============================================================================
>>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java (added)
>>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java Tue Nov 14 19:09:34 2017
>>> @@ -0,0 +1,106 @@
>>> +/*
>>> + * 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.felix.http.jetty.internal;
>>> +
>>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>>> +import org.eclipse.jetty.server.Request;
>>> +import org.eclipse.jetty.server.RequestLog;
>>> +import org.eclipse.jetty.server.Response;
>>> +import org.osgi.framework.*;
>>> +import org.osgi.util.tracker.ServiceTracker;
>>> +
>>> +import java.util.Map;
>>> +import java.util.concurrent.ConcurrentHashMap;
>>> +import java.util.concurrent.ConcurrentMap;
>>> +
>>> +/**
>>> + * An instance of Jetty's RequestLog that dispatches to registered RequestLog services in the service registry. A filter
>>> + * can be provided so that it only dispatches to selected services.
>>> + * <p>
>>> + * Unchecked exceptions from the RequestLog services are caught and logged to the OSGi LogService. to avoid flooding the
>>> + * LogService, we will remove a RequestLog service if it breaches a maximum number of errors (see {@link
>>> + * RequestLogTracker#MAX_ERROR_COUNT}). Once this happens we will stop dispatching to that service entirely until it is
>>> + * unregistered.
>>> + */
>>> +class RequestLogTracker extends ServiceTracker<RequestLog, RequestLog>  implements RequestLog {
>>> +
>>> +    private static final int MAX_ERROR_COUNT = 100;
>>> +
>>> +    private final ConcurrentMap<ServiceReference<?>, RequestLog> logSvcs = new ConcurrentHashMap<>();
>>> +    private final ConcurrentMap<ServiceReference<?>, Integer> naughtyStep = new ConcurrentHashMap<>();
>>> +
>>> +    RequestLogTracker(BundleContext context, String filter) throws InvalidSyntaxException {
>>> +        super(context, buildFilter(filter), null);
>>> +    }
>>> +
>>> +    private static Filter buildFilter(String inputFilter) throws InvalidSyntaxException {
>>> +        String objectClassFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, RequestLog.class.getName());
>>> +        String compositeFilter;
>>> +        if (inputFilter != null) {
>>> +            // Parse the input filter just for validation before we insert into ours.
>>> +            FrameworkUtil.createFilter(inputFilter);
>>> +            compositeFilter = "(&" + objectClassFilter + inputFilter + ")";
>>> +        } else {
>>> +            compositeFilter = objectClassFilter;
>>> +        }
>>> +        return FrameworkUtil.createFilter(compositeFilter);
>>> +    }
>>> +
>>> +    @Override
>>> +    public RequestLog addingService(ServiceReference<RequestLog> reference) {
>>> +        RequestLog logSvc = context.getService(reference);
>>> +        logSvcs.put(reference, logSvc);
>>> +        return logSvc;
>>> +    }
>>> +
>>> +    @Override
>>> +    public void removedService(ServiceReference<RequestLog> reference, RequestLog logSvc) {
>>> +        logSvcs.remove(reference);
>>> +        naughtyStep.remove(reference);
>>> +        context.ungetService(reference);
>>> +    }
>>> +
>>> +    @Override
>>> +    public void log(Request request, Response response) {
>>> +        for (Map.Entry<ServiceReference<?>, RequestLog> entry : logSvcs.entrySet()) {
>>> +            try {
>>> +                entry.getValue().log(request, response);
>>> +            } catch (Exception e) {
>>> +                processError(entry.getKey(), e);
>>> +            }
>>> +        }
>>> +    }
>>> +
>>> +    /**
>>> +     * Process an exception from a RequestLog service instance, and remove the service if it has reached the maximum
>>> +     * error limit.
>>> +     */
>>> +    private void processError(ServiceReference<?> reference, Exception e) {
>>> +        SystemLogger.error(reference, String.format("Error dispatching to request log service ID %d from bundle %s:%s",
>>> +                reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), e);
>>> +
>>> +        int naughty = naughtyStep.merge(reference, 1, Integer::sum);
>>> +        if (naughty >= MAX_ERROR_COUNT) {
>>> +            // We have reached (but not exceeded) the maximum, and the last error has been logged. Remove from the maps
>>> +            // so we will not invoke the service again.
>>> +            logSvcs.remove(reference);
>>> +            naughtyStep.remove(reference);
>>> +            SystemLogger.error(reference, String.format("RequestLog service ID %d from bundle %s:%s threw too many errors, it will no longer be invoked.",
>>> +                    reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), null);
>>> +        }
>>> +    }
>>> +}
>>>
>>> Added: felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
>>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java?rev=1815249&view=auto
>>> ==============================================================================
>>> --- felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java (added)
>>> +++ felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java Tue Nov 14 19:09:34 2017
>>> @@ -0,0 +1,97 @@
>>> +/*
>>> + * 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.felix.http.jetty.internal;
>>> +
>>> +import org.eclipse.jetty.server.Request;
>>> +import org.eclipse.jetty.server.RequestLog;
>>> +import org.eclipse.jetty.server.Response;
>>> +import org.junit.Test;
>>> +import org.junit.runner.RunWith;
>>> +import org.mockito.Mock;
>>> +import org.mockito.runners.MockitoJUnitRunner;
>>> +import org.osgi.framework.*;
>>> +
>>> +import java.util.concurrent.atomic.AtomicInteger;
>>> +
>>> +import static org.junit.Assert.assertEquals;
>>> +import static org.mockito.Mockito.*;
>>> +
>>> +@RunWith(MockitoJUnitRunner.class)
>>> +public class RequestLogTrackerTest {
>>> +
>>> +    @Mock
>>> +    BundleContext context;
>>> +
>>> +    @Test
>>> +    public void testInvokeRequestLog() throws Exception {
>>> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
>>> +
>>> +        RequestLog mockRequestLog = mock(RequestLog.class);
>>> +
>>> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
>>> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
>>> +
>>> +        // These invocations not passed through to the mock because it is not registered yet
>>> +        for (int i = 0; i < 10; i++)
>>> +            tracker.log(null, null);
>>> +
>>> +        tracker.addingService(mockSvcRef);
>>> +
>>> +        // These will pass through
>>> +        for (int i = 0; i < 15; i++)
>>> +            tracker.log(null, null);
>>> +
>>> +        tracker.removedService(mockSvcRef, mockRequestLog);
>>> +
>>> +        // And these will not.
>>> +        for (int i = 0; i < 50; i++)
>>> +            tracker.log(null, null);
>>> +
>>> +        verify(mockRequestLog, times(15)).log(isNull(Request.class), isNull(Response.class));
>>> +    }
>>> +
>>> +    @Test
>>> +    public void testNaughtyService() throws Exception {
>>> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
>>> +
>>> +        AtomicInteger counter = new AtomicInteger(0);
>>> +        RequestLog mockRequestLog = new RequestLog() {
>>> +            @Override
>>> +            public void log(Request request, Response response) {
>>> +                counter.addAndGet(1);
>>> +                throw new RuntimeException("This service always explodes");
>>> +            }
>>> +        };
>>> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
>>> +        Bundle mockBundle = mock(Bundle.class);
>>> +        when(mockSvcRef.getBundle()).thenReturn(mockBundle);
>>> +        when(mockBundle.getSymbolicName()).thenReturn("org.example");
>>> +        when(mockBundle.getVersion()).thenReturn(new Version("1.0.0"));
>>> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
>>> +
>>> +        tracker.addingService(mockSvcRef);
>>> +
>>> +        // Invoke 200 times
>>> +        for (int i = 0; i < 200; i++)
>>> +            tracker.log(null, null);
>>> +
>>> +        tracker.removedService(mockSvcRef, mockRequestLog);
>>> +
>>> +        // Invoked 100 times and then removed
>>> +        assertEquals(100, counter.get());
>>> +    }
>>> +}
>>>
>>>
>> --
>> Carsten Ziegeler
>> Adobe Research Switzerland
>> cziegeler@apache.org
>



-- 
Karl Pauls
karlpauls@gmail.com

Re: svn commit: r1815249 - in /felix/trunk/http/jetty: ./ src/main/java/org/apache/felix/http/jetty/internal/ src/test/java/org/apache/felix/http/jetty/internal/

Posted by "Neil Bartlett (Paremus)" <ne...@paremus.com>.
> On 14 Nov 2017, at 19:32, Carsten Ziegeler <cz...@apache.org> wrote:
> 
> Hi,
> 
> do we have a Jira issue covering this?

No this didn’t come from a JIRA.

> 
> Could you please also apply the patch at the R7 branch so they don't get
> out of sync?

Sure, will do.

Neil

> 
> Thanks
> 
> Carsten
> 
> 
> Nbartlett wrote
>> Author: nbartlett
>> Date: Tue Nov 14 19:09:34 2017
>> New Revision: 1815249
>> 
>> URL: http://svn.apache.org/viewvc?rev=1815249&view=rev
>> Log:
>> Implement HTTP request logging. This closes #127
>> 
>> Added:
>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
>>    felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
>> Modified:
>>    felix/trunk/http/jetty/pom.xml
>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
>>    felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
>> 
>> Modified: felix/trunk/http/jetty/pom.xml
>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/pom.xml?rev=1815249&r1=1815248&r2=1815249&view=diff
>> ==============================================================================
>> --- felix/trunk/http/jetty/pom.xml (original)
>> +++ felix/trunk/http/jetty/pom.xml Tue Nov 14 19:09:34 2017
>> @@ -38,6 +38,8 @@
>>     </scm>
>> 
>>     <properties>
>> +        <!-- Skip because of problems with Java 8 -->
>> +        <animal.sniffer.skip>true</animal.sniffer.skip>
>>         <felix.java.version>8</felix.java.version>
>>         <jetty.version>9.3.22.v20171030</jetty.version>
>>     </properties>
>> 
>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java
>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java?rev=1815249&view=auto
>> ==============================================================================
>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java (added)
>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/FileRequestLog.java Tue Nov 14 19:09:34 2017
>> @@ -0,0 +1,97 @@
>> +/*
>> + * 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.felix.http.jetty.internal;
>> +
>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>> +import org.eclipse.jetty.server.*;
>> +import org.osgi.framework.BundleContext;
>> +import org.osgi.framework.ServiceRegistration;
>> +
>> +import java.io.File;
>> +import java.io.IOException;
>> +import java.nio.file.Files;
>> +import java.nio.file.attribute.PosixFilePermission;
>> +import java.nio.file.attribute.PosixFilePermissions;
>> +import java.util.Dictionary;
>> +import java.util.Hashtable;
>> +
>> +class FileRequestLog {
>> +
>> +    public static final String SVC_PROP_NAME = "name";
>> +    public static final String DEFAULT_NAME = "file";
>> +    public static final String SVC_PROP_FILEPATH = "filepath";
>> +
>> +    private final NCSARequestLog delegate;
>> +    private final String logFilePath;
>> +    private final String serviceName;
>> +    private ServiceRegistration<RequestLog> registration = null;
>> +
>> +    FileRequestLog(JettyConfig config) {
>> +        logFilePath = config.getRequestLogFilePath();
>> +        serviceName = config.getRequestLogFileServiceName() != null ? config.getRequestLogFileServiceName() : DEFAULT_NAME;
>> +        if (config.isRequestLogFileAsync()) {
>> +            delegate = new AsyncNCSARequestLog(logFilePath);
>> +        } else {
>> +            delegate = new NCSARequestLog(logFilePath);
>> +        }
>> +
>> +        delegate.setAppend(config.isRequestLogFileAppend());
>> +        delegate.setRetainDays(config.getRequestLogFileRetainDays());
>> +        delegate.setFilenameDateFormat(config.getRequestLogFilenameDateFormat());
>> +        delegate.setExtended(config.isRequestLogFileExtended());
>> +        delegate.setIgnorePaths(config.getRequestLogFileIgnorePaths());
>> +        delegate.setLogCookies(config.isRequestLogFileLogCookies());
>> +        delegate.setLogServer(config.isRequestLogFileLogServer());
>> +        delegate.setLogLatency(config.isRequestLogFileLogLatency());
>> +    }
>> +
>> +    synchronized void start(BundleContext context) throws IOException, IllegalStateException {
>> +        File logFile = new File(logFilePath).getAbsoluteFile();
>> +        File logFileDir = logFile.getParentFile();
>> +        if (logFileDir != null && !logFileDir.isDirectory()) {
>> +            SystemLogger.info("Creating directory " + logFileDir.getAbsolutePath());
>> +            Files.createDirectories(logFileDir.toPath(), PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
>> +        }
>> +
>> +        if (registration != null) {
>> +            throw new IllegalStateException(getClass().getSimpleName() + " is already started");
>> +        }
>> +        try {
>> +            delegate.start();
>> +            Dictionary<String, Object> svcProps = new Hashtable<>();
>> +            svcProps.put(SVC_PROP_NAME, serviceName);
>> +            svcProps.put(SVC_PROP_FILEPATH, logFilePath);
>> +            registration = context.registerService(RequestLog.class, delegate, svcProps);
>> +        } catch (Exception e) {
>> +            SystemLogger.error("Error starting File Request Log", e);
>> +        }
>> +    }
>> +
>> +    synchronized void stop() {
>> +        try {
>> +            if (registration != null) {
>> +                registration.unregister();
>> +            }
>> +            delegate.stop();;
>> +        } catch (Exception e) {
>> +            SystemLogger.error("Error shutting down File Request Log", e);
>> +        } finally {
>> +            registration = null;
>> +        }
>> +    }
>> +
>> +}
>> 
>> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java
>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java?rev=1815249&r1=1815248&r2=1815249&view=diff
>> ==============================================================================
>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java (original)
>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyConfig.java Tue Nov 14 19:09:34 2017
>> @@ -165,6 +165,51 @@ public final class JettyConfig
>>     /** Felix specific property to set HTTP instance name. */
>>     public static final String FELIX_HTTP_SERVICE_NAME = "org.apache.felix.http.name";
>> 
>> +    /** Felix specific property to configure a filter for RequestLog services */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILTER = "org.apache.felix.http.requestlog.filter";
>> +
>> +    /** Felix specific property to enable request logging to the OSGi Log Service */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE = "org.apache.felix.http.requestlog.osgi.enable";
>> +
>> +    /** Felix specific property to specify the published "name" property of the OSGi Log Service-base Request Log service. Allows server configs to filter on specific log services. */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME = "org.apache.felix.http.requestlog.osgi.name";
>> +
>> +    /** Felix specific property to control the level of the log messages generated by the OSGi Log Service-based request log. Values must correspond to the constants defined in the LogService interface, default is 3 "INFO". */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL = "org.apache.felix.http.requestlog.osgi.level";
>> +
>> +    /** Felix specific property to enable request logging to a file and provide the path to that file. Default is null meaning that the file log is disabled. */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_PATH = "org.apache.felix.http.requestlog.file.path";
>> +
>> +    /** Felix specific property to specify the published "name" property of the file-based RequestLog service. Allows server configs to filter on specific log services. */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME = "org.apache.felix.http.requestlog.file.name";
>> +
>> +    /** Felix specific property to enable file request logging to be asynchronous */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_ASYNC = "org.apache.felix.http.requestlog.file.async";
>> +
>> +    /** Felix specific property to enable request logging to append to the log file rather than overwriting */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_APPEND = "org.apache.felix.http.requestlog.file.append";
>> +
>> +    /** Felix specific property to specify the number of days the request log file is retained */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS = "org.apache.felix.http.requestlog.file.retaindays";
>> +
>> +    /** Felix specific property to specify the date format in request log file names */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT = "org.apache.felix.http.requestlog.file.dateformat";
>> +
>> +    /** Felix specific property to enable extended request logging to a named file */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED = "org.apache.felix.http.requestlog.file.extended";
>> +
>> +    /** Felix specific property to ignore matching paths in the request log file */
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS = "org.apache.felix.http.requestlog.file.ignorepaths";
>> +
>> +    /** Felix specific property to enable request logging cookies in the request log file*/
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES = "org.apache.felix.http.requestlog.file.logcookies";
>> +
>> +    /** Felix specific property to enable request logging the host name in the request log file*/
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER = "org.apache.felix.http.requestlog.file.logserver";
>> +
>> +    /** Felix specific property to enable request logging request processing time in the request log file*/
>> +    public static final String FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY = "org.apache.felix.http.requestlog.file.loglatency";
>> +
>>     /** Felix specific property to define custom properties for the http runtime service. */
>>     public static final String FELIX_CUSTOM_HTTP_RUNTIME_PROPERTY_PREFIX = "org.apache.felix.http.runtime.init.";
>> 
>> @@ -425,6 +470,66 @@ public final class JettyConfig
>>     	return (String) getProperty(FELIX_HTTP_SERVICE_NAME);
>>     }
>> 
>> +    public String getRequestLogFilter() {
>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILTER, null);
>> +    }
>> +
>> +    public boolean isRequestLogOSGiEnabled() {
>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_OSGI_ENABLE, false);
>> +    }
>> +
>> +    public String getRequestLogOSGiServiceName() {
>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_OSGI_SERVICE_NAME);
>> +    }
>> +
>> +    public int getRequestLogOSGiLevel() {
>> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_OSGI_LEVEL, 3); // 3 == LogService.LOG_INFO
>> +    }
>> +
>> +    public String getRequestLogFilePath() {
>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_PATH, null);
>> +    }
>> +
>> +    public String getRequestLogFileServiceName() {
>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_SERVICE_NAME, "file");
>> +    }
>> +
>> +    public boolean isRequestLogFileAsync() {
>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_ASYNC, false);
>> +    }
>> +
>> +    public boolean isRequestLogFileAppend() {
>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_APPEND, true);
>> +    }
>> +
>> +    public int getRequestLogFileRetainDays() {
>> +        return getIntProperty(FELIX_HTTP_REQUEST_LOG_FILE_RETAIN_DAYS, 31);
>> +    }
>> +
>> +    public String getRequestLogFilenameDateFormat() {
>> +        return (String) getProperty(FELIX_HTTP_REQUEST_LOG_FILE_FILENAME_DATE_FORMAT, null);
>> +    }
>> +
>> +    public boolean isRequestLogFileExtended() {
>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_EXTENDED, false);
>> +    }
>> +
>> +    public String[] getRequestLogFileIgnorePaths() {
>> +        return getStringArrayProperty(FELIX_HTTP_REQUEST_LOG_FILE_IGNORE_PATHS, new String[0]);
>> +    }
>> +
>> +    public boolean isRequestLogFileLogCookies() {
>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_COOKIES, false);
>> +    }
>> +
>> +    public boolean isRequestLogFileLogServer() {
>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_SERVER, false);
>> +    }
>> +
>> +    public boolean isRequestLogFileLogLatency() {
>> +        return getBooleanProperty(FELIX_HTTP_REQUEST_LOG_FILE_LOG_LATENCY, false);
>> +    }
>> +
>>     public void reset()
>>     {
>>         update(null);
>> @@ -668,4 +773,5 @@ public final class JettyConfig
>>             return dflt;
>>         }
>>     }
>> +
>> }
>> 
>> Modified: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java
>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java?rev=1815249&r1=1815248&r2=1815249&view=diff
>> ==============================================================================
>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java (original)
>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/JettyService.java Tue Nov 14 19:09:34 2017
>> @@ -101,11 +101,15 @@ public final class JettyService extends
>>     private volatile BundleTracker<Deployment> bundleTracker;
>>     private volatile ServiceTracker<EventAdmin, EventAdmin> eventAdmintTracker;
>>     private volatile ConnectorFactoryTracker connectorTracker;
>> +    private volatile RequestLogTracker requestLogTracker;
>> +    private volatile LogServiceRequestLog osgiRequestLog;
>> +    private volatile FileRequestLog fileRequestLog;
>>     private volatile LoadBalancerCustomizerFactoryTracker loadBalancerCustomizerTracker;
>>     private volatile CustomizerWrapper customizerWrapper;
>>     private volatile EventAdmin eventAdmin;
>>     private boolean registerManagedService = true;
>> 
>> +
>>     public JettyService(final BundleContext context,
>>             final HttpServiceController controller)
>>     {
>> @@ -292,6 +296,22 @@ public final class JettyService extends
>>             this.controller.getEventDispatcher().setActive(false);
>>             this.controller.unregister();
>> 
>> +            if (this.fileRequestLog != null)
>> +            {
>> +                this.fileRequestLog.stop();
>> +                this.fileRequestLog = null;
>> +            }
>> +            if (this.osgiRequestLog != null)
>> +            {
>> +                this.osgiRequestLog.unregister();
>> +                this.osgiRequestLog = null;
>> +            }
>> +            if (this.requestLogTracker != null)
>> +            {
>> +                this.requestLogTracker.close();
>> +                this.requestLogTracker = null;
>> +            }
>> +
>>             if (this.connectorTracker != null)
>>             {
>>                 this.connectorTracker.close();
>> @@ -413,6 +433,26 @@ public final class JettyService extends
>>                 this.stopJetty();
>>                 SystemLogger.error("Jetty stopped (no connectors available)", null);
>>             }
>> +
>> +            try {
>> +                this.requestLogTracker = new RequestLogTracker(this.context, this.config.getRequestLogFilter());
>> +                this.requestLogTracker.open();
>> +                this.server.setRequestLog(requestLogTracker);
>> +            } catch (InvalidSyntaxException e) {
>> +                SystemLogger.error("Invalid filter syntax in request log tracker", e);
>> +            }
>> +
>> +            if (this.config.isRequestLogOSGiEnabled()) {
>> +                this.osgiRequestLog = new LogServiceRequestLog(this.config);
>> +                this.osgiRequestLog.register(this.context);
>> +                SystemLogger.info("Directing Jetty request logs to the OSGi Log Service");
>> +            }
>> +
>> +            if (this.config.getRequestLogFilePath() != null && !this.config.getRequestLogFilePath().isEmpty()) {
>> +                this.fileRequestLog = new FileRequestLog(config);
>> +                this.fileRequestLog.start(this.context);
>> +                SystemLogger.info("Directing Jetty request logs to " + this.config.getRequestLogFilePath());
>> +            }
>>         }
>>         else
>>         {
>> 
>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java
>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java?rev=1815249&view=auto
>> ==============================================================================
>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java (added)
>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/LogServiceRequestLog.java Tue Nov 14 19:09:34 2017
>> @@ -0,0 +1,80 @@
>> +/*
>> + * 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.felix.http.jetty.internal;
>> +
>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>> +import org.eclipse.jetty.server.AbstractNCSARequestLog;
>> +import org.eclipse.jetty.server.RequestLog;
>> +import org.osgi.framework.BundleContext;
>> +import org.osgi.framework.ServiceRegistration;
>> +
>> +import java.io.IOException;
>> +import java.util.Dictionary;
>> +import java.util.Hashtable;
>> +import java.util.concurrent.atomic.AtomicReference;
>> +
>> +/**
>> + * A RequestLog that logs to the OSGi LogService when present. Not registered by default.
>> + */
>> +class LogServiceRequestLog extends AbstractNCSARequestLog {
>> +
>> +    public static final String SVC_PROP_NAME = "name";
>> +    public static final String DEFAULT_NAME = "osgi";
>> +    public static final String PREFIX = "REQUEST: ";
>> +
>> +    private static final int DEFAULT_LOG_LEVEL = 3; // LogService.LOG_INFO
>> +
>> +    private final int logLevel;
>> +    private final String serviceName;
>> +
>> +    private ServiceRegistration<RequestLog> registration;
>> +
>> +    LogServiceRequestLog(JettyConfig config) {
>> +        this.serviceName = config.getRequestLogOSGiServiceName();
>> +        this.logLevel = config.getRequestLogOSGiLevel();
>> +    }
>> +
>> +    public synchronized void register(BundleContext context) throws IllegalStateException {
>> +        if (registration != null) {
>> +            throw new IllegalStateException(getClass().getSimpleName() + " already registered");
>> +        }
>> +        Dictionary<String, Object> svcProps = new Hashtable<>();
>> +        svcProps.put(SVC_PROP_NAME, serviceName);
>> +        this.registration = context.registerService(RequestLog.class, this, svcProps);
>> +    }
>> +
>> +    public synchronized void unregister() {
>> +        try {
>> +            if (registration != null) {
>> +                registration.unregister();;
>> +            }
>> +        } finally {
>> +            registration = null;
>> +        }
>> +    }
>> +
>> +    @Override
>> +    public void write(String s) throws IOException {
>> +        SystemLogger.info(PREFIX + s);
>> +    }
>> +
>> +    @Override
>> +    protected boolean isEnabled() {
>> +        return true;
>> +    }
>> +
>> +}
>> 
>> Added: felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java
>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java?rev=1815249&view=auto
>> ==============================================================================
>> --- felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java (added)
>> +++ felix/trunk/http/jetty/src/main/java/org/apache/felix/http/jetty/internal/RequestLogTracker.java Tue Nov 14 19:09:34 2017
>> @@ -0,0 +1,106 @@
>> +/*
>> + * 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.felix.http.jetty.internal;
>> +
>> +import org.apache.felix.http.base.internal.logger.SystemLogger;
>> +import org.eclipse.jetty.server.Request;
>> +import org.eclipse.jetty.server.RequestLog;
>> +import org.eclipse.jetty.server.Response;
>> +import org.osgi.framework.*;
>> +import org.osgi.util.tracker.ServiceTracker;
>> +
>> +import java.util.Map;
>> +import java.util.concurrent.ConcurrentHashMap;
>> +import java.util.concurrent.ConcurrentMap;
>> +
>> +/**
>> + * An instance of Jetty's RequestLog that dispatches to registered RequestLog services in the service registry. A filter
>> + * can be provided so that it only dispatches to selected services.
>> + * <p>
>> + * Unchecked exceptions from the RequestLog services are caught and logged to the OSGi LogService. to avoid flooding the
>> + * LogService, we will remove a RequestLog service if it breaches a maximum number of errors (see {@link
>> + * RequestLogTracker#MAX_ERROR_COUNT}). Once this happens we will stop dispatching to that service entirely until it is
>> + * unregistered.
>> + */
>> +class RequestLogTracker extends ServiceTracker<RequestLog, RequestLog>  implements RequestLog {
>> +
>> +    private static final int MAX_ERROR_COUNT = 100;
>> +
>> +    private final ConcurrentMap<ServiceReference<?>, RequestLog> logSvcs = new ConcurrentHashMap<>();
>> +    private final ConcurrentMap<ServiceReference<?>, Integer> naughtyStep = new ConcurrentHashMap<>();
>> +
>> +    RequestLogTracker(BundleContext context, String filter) throws InvalidSyntaxException {
>> +        super(context, buildFilter(filter), null);
>> +    }
>> +
>> +    private static Filter buildFilter(String inputFilter) throws InvalidSyntaxException {
>> +        String objectClassFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, RequestLog.class.getName());
>> +        String compositeFilter;
>> +        if (inputFilter != null) {
>> +            // Parse the input filter just for validation before we insert into ours.
>> +            FrameworkUtil.createFilter(inputFilter);
>> +            compositeFilter = "(&" + objectClassFilter + inputFilter + ")";
>> +        } else {
>> +            compositeFilter = objectClassFilter;
>> +        }
>> +        return FrameworkUtil.createFilter(compositeFilter);
>> +    }
>> +
>> +    @Override
>> +    public RequestLog addingService(ServiceReference<RequestLog> reference) {
>> +        RequestLog logSvc = context.getService(reference);
>> +        logSvcs.put(reference, logSvc);
>> +        return logSvc;
>> +    }
>> +
>> +    @Override
>> +    public void removedService(ServiceReference<RequestLog> reference, RequestLog logSvc) {
>> +        logSvcs.remove(reference);
>> +        naughtyStep.remove(reference);
>> +        context.ungetService(reference);
>> +    }
>> +
>> +    @Override
>> +    public void log(Request request, Response response) {
>> +        for (Map.Entry<ServiceReference<?>, RequestLog> entry : logSvcs.entrySet()) {
>> +            try {
>> +                entry.getValue().log(request, response);
>> +            } catch (Exception e) {
>> +                processError(entry.getKey(), e);
>> +            }
>> +        }
>> +    }
>> +
>> +    /**
>> +     * Process an exception from a RequestLog service instance, and remove the service if it has reached the maximum
>> +     * error limit.
>> +     */
>> +    private void processError(ServiceReference<?> reference, Exception e) {
>> +        SystemLogger.error(reference, String.format("Error dispatching to request log service ID %d from bundle %s:%s",
>> +                reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), e);
>> +
>> +        int naughty = naughtyStep.merge(reference, 1, Integer::sum);
>> +        if (naughty >= MAX_ERROR_COUNT) {
>> +            // We have reached (but not exceeded) the maximum, and the last error has been logged. Remove from the maps
>> +            // so we will not invoke the service again.
>> +            logSvcs.remove(reference);
>> +            naughtyStep.remove(reference);
>> +            SystemLogger.error(reference, String.format("RequestLog service ID %d from bundle %s:%s threw too many errors, it will no longer be invoked.",
>> +                    reference.getProperty(Constants.SERVICE_ID), reference.getBundle().getSymbolicName(), reference.getBundle().getVersion()), null);
>> +        }
>> +    }
>> +}
>> 
>> Added: felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java
>> URL: http://svn.apache.org/viewvc/felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java?rev=1815249&view=auto
>> ==============================================================================
>> --- felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java (added)
>> +++ felix/trunk/http/jetty/src/test/java/org/apache/felix/http/jetty/internal/RequestLogTrackerTest.java Tue Nov 14 19:09:34 2017
>> @@ -0,0 +1,97 @@
>> +/*
>> + * 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.felix.http.jetty.internal;
>> +
>> +import org.eclipse.jetty.server.Request;
>> +import org.eclipse.jetty.server.RequestLog;
>> +import org.eclipse.jetty.server.Response;
>> +import org.junit.Test;
>> +import org.junit.runner.RunWith;
>> +import org.mockito.Mock;
>> +import org.mockito.runners.MockitoJUnitRunner;
>> +import org.osgi.framework.*;
>> +
>> +import java.util.concurrent.atomic.AtomicInteger;
>> +
>> +import static org.junit.Assert.assertEquals;
>> +import static org.mockito.Mockito.*;
>> +
>> +@RunWith(MockitoJUnitRunner.class)
>> +public class RequestLogTrackerTest {
>> +    
>> +    @Mock
>> +    BundleContext context;
>> +    
>> +    @Test
>> +    public void testInvokeRequestLog() throws Exception {
>> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
>> +
>> +        RequestLog mockRequestLog = mock(RequestLog.class);
>> +
>> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
>> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
>> +
>> +        // These invocations not passed through to the mock because it is not registered yet
>> +        for (int i = 0; i < 10; i++)
>> +            tracker.log(null, null);
>> +
>> +        tracker.addingService(mockSvcRef);
>> +
>> +        // These will pass through
>> +        for (int i = 0; i < 15; i++)
>> +            tracker.log(null, null);
>> +
>> +        tracker.removedService(mockSvcRef, mockRequestLog);
>> +
>> +        // And these will not.
>> +        for (int i = 0; i < 50; i++)
>> +            tracker.log(null, null);
>> +
>> +        verify(mockRequestLog, times(15)).log(isNull(Request.class), isNull(Response.class));
>> +    }
>> +
>> +    @Test
>> +    public void testNaughtyService() throws Exception {
>> +        RequestLogTracker tracker = new RequestLogTracker(context, null);
>> +
>> +        AtomicInteger counter = new AtomicInteger(0);
>> +        RequestLog mockRequestLog = new RequestLog() {
>> +            @Override
>> +            public void log(Request request, Response response) {
>> +                counter.addAndGet(1);
>> +                throw new RuntimeException("This service always explodes");
>> +            }
>> +        };
>> +        ServiceReference<RequestLog> mockSvcRef = mock(ServiceReference.class);
>> +        Bundle mockBundle = mock(Bundle.class);
>> +        when(mockSvcRef.getBundle()).thenReturn(mockBundle);
>> +        when(mockBundle.getSymbolicName()).thenReturn("org.example");
>> +        when(mockBundle.getVersion()).thenReturn(new Version("1.0.0"));
>> +        when(context.getService(mockSvcRef)).thenReturn(mockRequestLog);
>> +
>> +        tracker.addingService(mockSvcRef);
>> +
>> +        // Invoke 200 times
>> +        for (int i = 0; i < 200; i++)
>> +            tracker.log(null, null);
>> +
>> +        tracker.removedService(mockSvcRef, mockRequestLog);
>> +
>> +        // Invoked 100 times and then removed
>> +        assertEquals(100, counter.get());
>> +    }
>> +}
>> 
>> 
> -- 
> Carsten Ziegeler
> Adobe Research Switzerland
> cziegeler@apache.org