Allow TraceWebFilter to trace more attributes
Update TraceWebFilter to optionally trace more details from the HttpServletRequest/HttpServletResponse. The `management.trace.include` property can be used to change what aspects are logged. Closes gh-3948
This commit is contained in:
parent
143536f72d
commit
e3315d2252
|
@ -22,12 +22,14 @@ import javax.servlet.ServletRegistration;
|
|||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.trace.TraceProperties;
|
||||
import org.springframework.boot.actuate.trace.TraceRepository;
|
||||
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
|
@ -39,6 +41,7 @@ import org.springframework.web.servlet.DispatcherServlet;
|
|||
*/
|
||||
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, ServletRegistration.class })
|
||||
@AutoConfigureAfter(TraceRepositoryAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(TraceProperties.class)
|
||||
public class TraceWebFilterAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
|
@ -50,10 +53,15 @@ public class TraceWebFilterAutoConfiguration {
|
|||
@Value("${management.dump_requests:false}")
|
||||
private boolean dumpRequests;
|
||||
|
||||
@Autowired
|
||||
TraceProperties traceProperties = new TraceProperties();
|
||||
|
||||
@Bean
|
||||
public WebRequestTraceFilter webRequestLoggingFilter(BeanFactory beanFactory) {
|
||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository);
|
||||
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository,
|
||||
this.traceProperties);
|
||||
filter.setDumpRequests(this.dumpRequests);
|
||||
|
||||
if (this.errorAttributes != null) {
|
||||
filter.setErrorAttributes(this.errorAttributes);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.boot.actuate.trace;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Configuration properties for tracing.
|
||||
*
|
||||
* @author Wallace Wadge
|
||||
* @author Phillip Webb
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "management.trace")
|
||||
public class TraceProperties {
|
||||
|
||||
private static final Set<Include> DEFAULT_INCLUDES;
|
||||
|
||||
static {
|
||||
Set<Include> defaultIncludes = new LinkedHashSet<Include>();
|
||||
defaultIncludes.add(Include.REQUEST_HEADERS);
|
||||
defaultIncludes.add(Include.RESPONSE_HEADERS);
|
||||
defaultIncludes.add(Include.ERRORS);
|
||||
DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes);
|
||||
}
|
||||
|
||||
private static final int DEFAULT_MAX_CONTENT_LENGTH = 32768;
|
||||
|
||||
/**
|
||||
* Items to included in the trace. Defaults to request/response headers and errors.
|
||||
*/
|
||||
private Set<Include> include = new HashSet<Include>(DEFAULT_INCLUDES);
|
||||
|
||||
/**
|
||||
* Maximum number of content bytes that can be traced before being truncated (-1 for
|
||||
* unlimited).
|
||||
*/
|
||||
private int maxContentLength = DEFAULT_MAX_CONTENT_LENGTH;
|
||||
|
||||
public Set<Include> getInclude() {
|
||||
return this.include;
|
||||
}
|
||||
|
||||
public void setInclude(Set<Include> include) {
|
||||
this.include = include;
|
||||
}
|
||||
|
||||
public int getMaxContentLength() {
|
||||
return this.maxContentLength;
|
||||
}
|
||||
|
||||
public void setMaxContentLength(int maxContentLength) {
|
||||
this.maxContentLength = maxContentLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include options for tracing.
|
||||
*/
|
||||
public enum Include {
|
||||
|
||||
/**
|
||||
* Include request headers.
|
||||
*/
|
||||
REQUEST_HEADERS,
|
||||
|
||||
/**
|
||||
* Include response headers.
|
||||
*/
|
||||
RESPONSE_HEADERS,
|
||||
|
||||
/**
|
||||
* Include errors (if any).
|
||||
*/
|
||||
ERRORS,
|
||||
|
||||
/**
|
||||
* Include path info.
|
||||
*/
|
||||
PATH_INFO,
|
||||
|
||||
/**
|
||||
* Include the translated path.
|
||||
*/
|
||||
PATH_TRANSLATED,
|
||||
|
||||
/**
|
||||
* Include the context path.
|
||||
*/
|
||||
CONTEXT_PATH,
|
||||
|
||||
/**
|
||||
* Include the user principal.
|
||||
*/
|
||||
USER_PRINCIPAL,
|
||||
|
||||
/**
|
||||
* Include the parameters.
|
||||
*/
|
||||
PARAMETERS,
|
||||
|
||||
/**
|
||||
* Include the query string.
|
||||
*/
|
||||
QUERY_STRING,
|
||||
|
||||
/**
|
||||
* Include the authentication type.
|
||||
*/
|
||||
AUTH_TYPE,
|
||||
|
||||
/**
|
||||
* Include the remote address.
|
||||
*/
|
||||
REMOTE_ADDRESS,
|
||||
|
||||
/**
|
||||
* Include the session ID.
|
||||
*/
|
||||
SESSION_ID,
|
||||
|
||||
/**
|
||||
* Include the remote user.
|
||||
*/
|
||||
REMOTE_USER,
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.boot.actuate.trace;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashMap;
|
||||
|
@ -28,12 +29,13 @@ import javax.servlet.FilterChain;
|
|||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.boot.actuate.trace.TraceProperties.Include;
|
||||
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
|
@ -41,6 +43,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||
* Servlet {@link Filter} that logs all requests to a {@link TraceRepository}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Wallace Wadge
|
||||
*/
|
||||
public class WebRequestTraceFilter extends OncePerRequestFilter implements Ordered {
|
||||
|
||||
|
@ -52,16 +55,31 @@ public class WebRequestTraceFilter extends OncePerRequestFilter implements Order
|
|||
// enriched headers, but users can add stuff after this if they want to
|
||||
private int order = Ordered.LOWEST_PRECEDENCE - 10;
|
||||
|
||||
private final TraceRepository traceRepository;
|
||||
private final TraceRepository repository;
|
||||
|
||||
private ErrorAttributes errorAttributes;
|
||||
|
||||
private final TraceProperties properties;
|
||||
|
||||
/**
|
||||
* Create a new {@link WebRequestTraceFilter} instance.
|
||||
* @param traceRepository the trace repository
|
||||
* @param traceRepository the trace repository.
|
||||
* @deprecated since 1.3.0 in favor of
|
||||
* {@link #WebRequestTraceFilter(TraceRepository, TraceProperties)}
|
||||
*/
|
||||
@Deprecated
|
||||
public WebRequestTraceFilter(TraceRepository traceRepository) {
|
||||
this.traceRepository = traceRepository;
|
||||
this(traceRepository, new TraceProperties());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link WebRequestTraceFilter} instance.
|
||||
* @param repository the trace repository
|
||||
* @param properties the trace properties
|
||||
*/
|
||||
public WebRequestTraceFilter(TraceRepository repository, TraceProperties properties) {
|
||||
this.repository = repository;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,44 +104,54 @@ public class WebRequestTraceFilter extends OncePerRequestFilter implements Order
|
|||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
Map<String, Object> trace = getTrace(request);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Processing request " + request.getMethod() + " "
|
||||
+ request.getRequestURI());
|
||||
if (this.dumpRequests) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> headers = (Map<String, Object>) trace.get("headers");
|
||||
this.logger.trace("Headers: " + headers);
|
||||
}
|
||||
}
|
||||
|
||||
logTrace(request, trace);
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
finally {
|
||||
enhanceTrace(trace, response);
|
||||
this.traceRepository.add(trace);
|
||||
this.repository.add(trace);
|
||||
}
|
||||
}
|
||||
|
||||
protected void enhanceTrace(Map<String, Object> trace, HttpServletResponse response) {
|
||||
Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
for (String header : response.getHeaderNames()) {
|
||||
String value = response.getHeader(header);
|
||||
headers.put(header, value);
|
||||
}
|
||||
headers.put("status", "" + response.getStatus());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> allHeaders = (Map<String, Object>) trace.get("headers");
|
||||
allHeaders.put("response", headers);
|
||||
}
|
||||
|
||||
protected Map<String, Object> getTrace(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
Throwable exception = (Throwable) request
|
||||
.getAttribute("javax.servlet.error.exception");
|
||||
Principal userPrincipal = request.getUserPrincipal();
|
||||
Map<String, Object> trace = new LinkedHashMap<String, Object>();
|
||||
Map<String, Object> headers = new LinkedHashMap<String, Object>();
|
||||
trace.put("method", request.getMethod());
|
||||
trace.put("path", request.getRequestURI());
|
||||
trace.put("headers", headers);
|
||||
if (isIncluded(Include.REQUEST_HEADERS)) {
|
||||
headers.put("request", getRequestHeaders(request));
|
||||
}
|
||||
add(trace, Include.PATH_INFO, "pathInfo", request.getPathInfo());
|
||||
add(trace, Include.PATH_TRANSLATED, "pathTranslated",
|
||||
request.getPathTranslated());
|
||||
add(trace, Include.CONTEXT_PATH, "contextPath", request.getContextPath());
|
||||
add(trace, Include.USER_PRINCIPAL, "userPrincipal",
|
||||
(userPrincipal == null ? null : userPrincipal.getName()));
|
||||
add(trace, Include.PARAMETERS, "parameters", request.getParameterMap());
|
||||
add(trace, Include.QUERY_STRING, "query", request.getQueryString());
|
||||
add(trace, Include.AUTH_TYPE, "authType", request.getAuthType());
|
||||
add(trace, Include.REMOTE_ADDRESS, "remoteAddress", request.getRemoteAddr());
|
||||
add(trace, Include.SESSION_ID, "sessionId",
|
||||
(session == null ? null : session.getId()));
|
||||
add(trace, Include.REMOTE_USER, "remoteUser", request.getRemoteUser());
|
||||
if (isIncluded(Include.ERRORS) && exception != null
|
||||
&& this.errorAttributes != null) {
|
||||
trace.put("error", this.errorAttributes
|
||||
.getErrorAttributes(new ServletRequestAttributes(request), true));
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
private Map<String, Object> getRequestHeaders(HttpServletRequest request) {
|
||||
Map<String, Object> headers = new LinkedHashMap<String, Object>();
|
||||
Enumeration<String> names = request.getHeaderNames();
|
||||
|
||||
while (names.hasMoreElements()) {
|
||||
String name = names.nextElement();
|
||||
List<String> values = Collections.list(request.getHeaders(name));
|
||||
|
@ -135,23 +163,45 @@ public class WebRequestTraceFilter extends OncePerRequestFilter implements Order
|
|||
value = "";
|
||||
}
|
||||
headers.put(name, value);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void enhanceTrace(Map<String, Object> trace, HttpServletResponse response) {
|
||||
Map<String, Object> headers = (Map<String, Object>) trace.get("headers");
|
||||
headers.put("response", getResponseHeaders(response));
|
||||
}
|
||||
|
||||
private Map<String, String> getResponseHeaders(HttpServletResponse response) {
|
||||
Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
for (String header : response.getHeaderNames()) {
|
||||
String value = response.getHeader(header);
|
||||
headers.put(header, value);
|
||||
}
|
||||
Map<String, Object> trace = new LinkedHashMap<String, Object>();
|
||||
Map<String, Object> allHeaders = new LinkedHashMap<String, Object>();
|
||||
allHeaders.put("request", headers);
|
||||
trace.put("method", request.getMethod());
|
||||
trace.put("path", request.getRequestURI());
|
||||
trace.put("headers", allHeaders);
|
||||
Throwable exception = (Throwable) request
|
||||
.getAttribute("javax.servlet.error.exception");
|
||||
if (exception != null && this.errorAttributes != null) {
|
||||
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
|
||||
Map<String, Object> error = this.errorAttributes
|
||||
.getErrorAttributes(requestAttributes, true);
|
||||
trace.put("error", error);
|
||||
headers.put("status", "" + response.getStatus());
|
||||
return headers;
|
||||
}
|
||||
|
||||
private void logTrace(HttpServletRequest request, Map<String, Object> trace) {
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger.trace("Processing request " + request.getMethod() + " "
|
||||
+ request.getRequestURI());
|
||||
if (this.dumpRequests) {
|
||||
this.logger.trace("Headers: " + trace.get("headers"));
|
||||
}
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
private void add(Map<String, Object> trace, Include include, String name,
|
||||
Object value) {
|
||||
if (isIncluded(include) && value != null) {
|
||||
trace.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isIncluded(Include include) {
|
||||
return this.properties.getInclude().contains(include);
|
||||
}
|
||||
|
||||
public void setErrorAttributes(ErrorAttributes errorAttributes) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,9 +16,21 @@
|
|||
|
||||
package org.springframework.boot.actuate.trace;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.Cookie;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.boot.actuate.trace.TraceProperties.Include;
|
||||
import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
@ -29,35 +41,88 @@ import static org.junit.Assert.assertEquals;
|
|||
* Tests for {@link WebRequestTraceFilter}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Wallace Wadge
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class WebRequestTraceFilterTests {
|
||||
|
||||
private final WebRequestTraceFilter filter = new WebRequestTraceFilter(
|
||||
new InMemoryTraceRepository());
|
||||
private final InMemoryTraceRepository repository = new InMemoryTraceRepository();
|
||||
|
||||
private TraceProperties properties = new TraceProperties();
|
||||
|
||||
private WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
|
||||
this.properties);
|
||||
|
||||
@Test
|
||||
public void filterDumpsRequest() {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void filterAddsTraceWithDefaultIncludes() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
||||
request.addHeader("Accept", "application/json");
|
||||
Map<String, Object> trace = this.filter.getTrace(request);
|
||||
assertEquals("GET", trace.get("method"));
|
||||
assertEquals("/foo", trace.get("path"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> map = (Map<String, Object>) trace.get("headers");
|
||||
assertEquals("{Accept=application/json}", map.get("request").toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterDumpsResponse() {
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public void filterAddsTraceWithCustomIncludes() throws IOException, ServletException {
|
||||
this.properties.setInclude(EnumSet.allOf(Include.class));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
||||
request.addHeader("Accept", "application/json");
|
||||
request.setContextPath("some.context.path");
|
||||
request.setContent("Hello, World!".getBytes());
|
||||
request.setRemoteAddr("some.remote.addr");
|
||||
request.setQueryString("some.query.string");
|
||||
request.setParameter("param", "paramvalue");
|
||||
File tmp = File.createTempFile("spring-boot", "tmp");
|
||||
String url = tmp.toURI().toURL().toString();
|
||||
request.setPathInfo(url);
|
||||
tmp.deleteOnExit();
|
||||
Cookie cookie = new Cookie("testCookie", "testValue");
|
||||
request.setCookies(cookie);
|
||||
request.setAuthType("authType");
|
||||
Principal principal = new Principal() {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "principalTest";
|
||||
}
|
||||
|
||||
};
|
||||
request.setUserPrincipal(principal);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
response.addHeader("Content-Type", "application/json");
|
||||
Map<String, Object> trace = this.filter.getTrace(request);
|
||||
this.filter.enhanceTrace(trace, response);
|
||||
@SuppressWarnings("unchecked")
|
||||
this.filter.doFilterInternal(request, response, new FilterChain() {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response)
|
||||
throws IOException, ServletException {
|
||||
BufferedReader bufferedReader = request.getReader();
|
||||
while (bufferedReader.readLine() != null) {
|
||||
// read the contents as normal (forces cache to fill up)
|
||||
}
|
||||
response.getWriter().println("Goodbye, World!");
|
||||
}
|
||||
|
||||
});
|
||||
assertEquals(1, this.repository.findAll().size());
|
||||
Map<String, Object> trace = this.repository.findAll().iterator().next().getInfo();
|
||||
Map<String, Object> map = (Map<String, Object>) trace.get("headers");
|
||||
assertEquals("{Content-Type=application/json, status=200}",
|
||||
map.get("response").toString());
|
||||
assertEquals("GET", trace.get("method"));
|
||||
assertEquals("/foo", trace.get("path"));
|
||||
assertEquals("paramvalue",
|
||||
((String[]) ((Map) trace.get("parameters")).get("param"))[0]);
|
||||
assertEquals("some.remote.addr", trace.get("remoteAddress"));
|
||||
assertEquals("some.query.string", trace.get("query"));
|
||||
assertEquals(principal.getName(), trace.get("userPrincipal"));
|
||||
assertEquals("some.context.path", trace.get("contextPath"));
|
||||
assertEquals(url, trace.get("pathInfo"));
|
||||
assertEquals("authType", trace.get("authType"));
|
||||
assertEquals("{Accept=application/json}", map.get("request").toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -784,6 +784,9 @@ content into your application; rather pick only the properties that you need.
|
|||
management.health.solr.enabled=true
|
||||
management.health.status.order=DOWN, OUT_OF_SERVICE, UNKNOWN, UP
|
||||
|
||||
# TRACING (({sc-spring-boot-actuator}/trace/TraceProperties.{sc-ext}[TraceProperties])
|
||||
management.trace.include=request-headers,response-headers,errors # See TraceProperties.Include for options
|
||||
|
||||
# MVC ONLY ENDPOINTS
|
||||
endpoints.jolokia.path=/jolokia
|
||||
endpoints.jolokia.sensitive=true
|
||||
|
|
|
@ -22,3 +22,8 @@ info.group: @project.groupId@
|
|||
info.artifact: @project.artifactId@
|
||||
info.name: @project.name@
|
||||
info.version: @project.version@
|
||||
|
||||
management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,ERRORS,PATH_INFO,\
|
||||
PATH_TRANSLATED,CONTEXT_PATH,USER_PRINCIPAL,PARAMETERS,QUERY_STRING,AUTH_TYPE,\
|
||||
REMOTE_ADDRESS,SESSION_ID,REMOTE_USER
|
||||
|
||||
|
|
Loading…
Reference in New Issue