This module is intended to be used for Spring projects.
If you are using:
See releases for the latest release
<dependency>
<!-- add this dependency before work-tracker to avoid Logback dependency conflicts -->
<groupId>org.logback-extensions</groupId>
<artifactId>logback-ext-spring</artifactId>
<version>0.1.4</version>
</dependency>
<dependency>
<groupId>com.deere.isg.work-tracker</groupId>
<artifactId>work-tracker-spring</artifactId>
<version>${work-tracker.version}</version>
</dependency>Some Spring libraries and logging that may be required
<!-- Spring libraries -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<!-- logging libraries and bridges, ref: https://www.slf4j.org/legacy.html -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${logging.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${logging.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>${logging.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>${logging.version}</version>
</dependency>
<!-- If you plan to use logback.groovy, use Groovy 2.4.0 or latest -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.version}</version>
</dependency>See example for more details
requires com.deere.isg.worktracker.spring;
Create a ContextListener for WorkTracker
@Configuration
public class WorkTrackerContextListener extends WorkContextListener {
public WorkTrackerContextListener() {
super(new WorkConfig.Builder<SpringWork>(new OutstandingWork<>())
.withHttpFloodSensor() // omit if not needed
.withZombieDetector() // omit if not needed
.build());
}
}If you don't need Flood Sensor and/or Zombie protection, you can omit withHttpFloodSensor and/or withZombieDetector to remove those features. You will also need to remove their respective filters from web.xml.
Add the filters and listeners in web.xml
<context-param>
<param-name>logbackConfigLocation</param-name>
<param-value>classpath:logback.groovy</param-value>
</context-param>
<filter>
<filter-name>springWorkFilter</filter-name>
<filter-class>com.deere.isg.worktracker.spring.SpringWorkFilter</filter-class>
</filter>
<filter>
<filter-name>requestBouncerFilter</filter-name>
<filter-class>com.deere.isg.worktracker.servlet.RequestBouncerFilter</filter-class>
</filter>
<filter>
<filter-name>zombieFilter</filter-name>
<filter-class>com.deere.isg.worktracker.servlet.ZombieFilter</filter-class>
</filter>
<!-- filter mappings -->
<filter-mapping>
<filter-name>springWorkFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>requestBouncerFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>zombieFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- listeners -->
<listener>
<listener-class>ch.qos.logback.ext.spring.web.LogbackConfigListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- add your workTrackerContextListener -->
<listener>
<listener-class>com.example.WorkTrackerContextListener</listener-class>
</listener>Add the interceptors in applicationContext.xml
<mvc:interceptors>
<bean class="com.deere.isg.worktracker.spring.SpringLoggerHandlerInterceptor"/>
<bean class="com.deere.isg.worktracker.spring.SpringRequestBouncerHandlerInterceptor"/>
</mvc:interceptors>Provide a SpringWork subclass that overrides SpringWork#updateUserInformation(HttpServletRequest request) to add the user's
username to the remoteUser using Work#setRemoteUser(String). You can also add other information in the MDC, if you intend to use it as context, by using Work#addToMDC(String). Example:
WARNING: Please do not add any password to the MDC.
public class UserSpringWork extends SpringWork {
public UserSpringWork(ServletRequest request) {
super(request);
}
@Override
public void updateUserInformation(HttpServletRequest request) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
setRemoteUser(auth.getName());
}
}Because of Java Type Erasure, you should define a custom WorkFilter to take the UserSpringWork and discard SpringWorkFilter in your web.xml in favor of WorkFilter:
public class WorkFilter extends AbstractSpringWorkFilter<UserSpringWork> {
@Override
protected UserSpringWork createWork(ServletRequest request) {
return new UserSpringWork(request);
}
}<filter>
<filter-name>workFilter</filter-name>
<filter-class>com.example.WorkFilter</filter-class>
</filter>
<!--... -->
<filter-mapping>
<filter-name>workFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>Then add the SpringWorkPostAuthFilter to the filter list in web.xml after the login/Spring Security filters.
<filter>
<!-- Add this after the spring security filters, after the username is known -->
<filter-name>springWorkPostAuthFilter</filter-name>
<filter-class>com.deere.isg.worktracker.spring.SpringWorkPostAuthFilter</filter-class>
</filter>
<!--... -->
<filter-mapping>
<filter-name>springWorkPostAuthFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>Then your configuration will need UserSpringWork as the type, example:
@Configuration
public class WorkTrackerContextListener extends WorkContextListener {
public WorkTrackerContextListener() {
super(new WorkConfig.Builder<UserSpringWork>(new OutstandingWork<>())
.withHttpFloodSensor() // omit if not needed
.withZombieDetector() // omit if not needed
.build());
}
}Often times, you have a helper class that you can autowire to retrieve the username. In that case, you can define the UserSpringWork class as a component with prototype scope, as follows:
@Component
@Scope("prototype")
public class UserSpringWork extends SpringWork {
//...
}Then define SpringWorkPostAuthFilter as a bean and use DelegatingFilterProxy when declaring the filter in web.xml with targetFilterLifecycle set to true. This way SpringWorkPostAuthFilter will have Spring context.
<bean id="springWorkPostAuthFilter" class="com.deere.isg.worktracker.spring.SpringWorkPostAuthFilter"/>
<!-- or use @Bean for Java config --><filter>
<filter-name>springWorkPostAuthFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!--...-->
<filter-mapping>
<filter-name>springWorkPostAuthFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>Cleanse your metadata keys before adding them to the MDC by using the KeyCleanser interface. It provides you with the key, value, and the URI of the current request. PathMetadataCleanser is the default KeyCleanser.
NOTE: PathMetadataCleanser converts every key into snake_case since Work#addToMDC accepts only snake_case values.
PathMetadataCleanser has four steps: Reserved, Standard, Transform, and Banned:
Reservedis for the metadata that are already used in work-tracker and simple words that aren't good context keys. If the key falls in this category, the cleanser will add a context to the key from the path. Example: requests for the URI/user/{id}will becomeuser_idStandardis for converting keys with non-standard names into standardized keys. Example:comp_namecan becomecomplete_name. Add to this list using PathMetaDataCleanser.addStandard().Transformis a function you provide to convert keys that require more than just simple replacement, or apply a rule your application needs to apply to all keys.Bannedis for blacklisting keys. Some keys are not allowed inElasticsearchsince they are reserved and would cause failure if they are uploaded to Elasticsearch. Banned keys will convert those keys in order to prevent Elasticsearch log upload failures. You can also use it to convert restricted keys into different ones.
public KeyCleanser keyCleanser() {
PathMetadataCleanser cleanser = new PathMetadataCleanser();
cleanser.addStandard("short_id", "starndardized_id");
cleanser.setTransformFunction(key -> key + "_suffix");
cleanser.addBanned("banned", "good_id");
return cleanser;
}Then add it to your WorkFilter
Request Bouncer requires Connection Limits to determine whether to reject a work if the work exceeds a particular limit. By default, ConnectionLimits provides limits for same session, same user, same service and the total. You can also provide your own limits as follows:
@Configuration
public class WorkTrackerContextListener extends WorkContextListener {
public WorkTrackerContextListener() {
super(new WorkConfig.Builder<SpringWork>(new OutstandingWork<>())
.setHttpFloodSensorWithLimit(connectionLimits()) //add the connectionLimits here
.withZombieDetector()
.build());
}
public static ConnectionLimits<SpringWork> connectionLimits() {
ConnectionLimits<SpringWork> limits = new ConnectionLimits<>();
//limit, typeName and function
limits.addConnectionLimit(25, "service").method(SpringWork::getService);
//limit, typeName and Predicate
limits.addConnectionLimit(20, "acceptHeader").test(w -> w.getAcceptHeader()
.contains(MediaType.APPLICATION_XML_VALUE)
);
//limit, typeName and a dynamic predicate
limits.addConnectionLimit(10, "acceptHeader")
.buildTest(incoming -> (incoming.getService().contains("foo") ?
(w->incoming.getService().equals(w.getService())) :
(w->false)));
//limit, typeName and function to execute retry later calculation
limits.addConnectionLimit(2, USER_TYPE).advanced(incoming -> Optional.of(incoming.getElapsedMillis()));
//limit, typeName, floodSensor and function to execute retry later calculation
limits.addConnectionLimit(2, USER_TYPE).advanced((floodSensor, incoming) -> Optional.of(incoming.getElapsedMillis()));
return limits;
}
}- Interceptor for RestTemplates
We provide an HttpInterceptor that has Zombie protection for runaway requests (i.e. requests that can take too long and eventually become orphan because either they never return a value or the user has interrupted the request). This interceptor can be used with a RestTemplate or a similar database template in Spring. Add the following into your configuration class:
@Bean
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
template.getInterceptors().add(new ZombieHttpInterceptor());
return template;
}- MdcExecutor
Track your background tasks with the MdcExecutor. Example:
<!-- in applicationContext.xml, add the following -->
<!-- any executor customization is permitted -->
<task:executor id="executor" pool-size="20"/>// Add a bean to your Configuration
@Bean
public Executor mdcExecutor(Executor executor) {
return new MdcExecutor(executor);
}
// Use it with Autowiring
@Autowiring
private MdcExecutor mdcExecutor;
//...
mdcExecutor.execute(someRunnable);We provide a WorkHttpServlet that outputs all the outstanding work that are currently in progress. This can be used for debugging purposes. Below is the configuration in web.xml:
<servlet>
<servlet-name>workHttpServlet</servlet-name>
<servlet-class>com.deere.isg.worktracker.spring.SpringWorkHttpServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>workHttpServlet</servlet-name>
<url-pattern>/health/outstanding</url-pattern>
</servlet-mapping>Helper class for autowiring OutstandingWork, ZombieDetector and HttpFloodSensor:
@Configuration
public class WorkContext implements ServletContextAware {
private OutstandingWork<SpringWork> outstanding;
private HttpFloodSensor<SpringWork> floodSensor;
private ZombieDetector detector;
@Override
@SuppressWarnings("unchecked")
public void setServletContext(ServletContext servletContext) {
this.outstanding = (OutstandingWork<SpringWork>) servletContext.getAttribute(OUTSTANDING_ATTR);
this.floodSensor = (HttpFloodSensor<SpringWork>) servletContext.getAttribute(FLOOD_SENSOR_ATTR);
this.detector = (ZombieDetector) servletContext.getAttribute(ZOMBIE_ATTR);
}
@Bean
public OutstandingWork<SpringWork> outstanding() {
return outstanding;
}
@Bean
public ZombieDetector detector() {
return detector;
}
@Bean
public HttpFloodSensor<SpringWork> floodSensor() {
return floodSensor;
}
}Then using WorkContext:
//field injection. Can also use Constructor injection
@Autowired
private OutstandingWork<SpringWork> outstanding;
outstanding.putInContext("some_key", "some value");
//...
@Autowired
private ZombieDetector detector;
detector.killRunaway();
//...