com.netflix.spectator.sandbox.HttpLogEntry Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2014-2019 Netflix, Inc.
*
* 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 com.netflix.spectator.sandbox;
import com.netflix.spectator.api.Id;
import com.netflix.spectator.api.Registry;
import com.netflix.spectator.api.Spectator;
import com.netflix.spectator.impl.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Helper for logging http request related information.
*
* @deprecated Moved to {@code com.netflix.spectator.ipc.http} package. This is now just a
* thin wrapper to preserve compatibility. This class is scheduled for removal in a future release.
*/
@Deprecated
public class HttpLogEntry {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpLogEntry.class);
private static final Marker CLIENT = MarkerFactory.getMarker("http-client");
private static final Marker SERVER = MarkerFactory.getMarker("http-server");
private static final Registry REGISTRY = Spectator.globalRegistry();
private static final Id COMPLETE = REGISTRY.createId("http.req.complete");
private static final Id ATTEMPT = REGISTRY.createId("http.req.attempt");
private static final Id REQ_HEADER_SIZE = REGISTRY.createId("http.req.headerSize");
private static final Id REQ_ENTITY_SIZE = REGISTRY.createId("http.req.entitySize");
private static final Id RES_HEADER_SIZE = REGISTRY.createId("http.res.headerSize");
private static final Id RES_ENTITY_SIZE = REGISTRY.createId("http.res.entitySize");
private static final BucketFunction BUCKETS =
BucketFunctions.latency(maxLatency(), TimeUnit.MILLISECONDS);
/**
* Including the endpoint is useful, but we need to be careful about the number of
* matches. A fixed prefix list is fairly easy to use and makes the number and set of matches
* explicit.
*/
private static final List ENDPOINT_PREFIXES = parseEndpoints(endpointPrefixes());
private static long maxLatency() {
return Long.parseLong(System.getProperty("spectator.http.maxLatency", "8000"));
}
private static String endpointPrefixes() {
return System.getProperty("spectator.http.endpointPrefixes", "/healthcheck");
}
private static List parseEndpoints(String s) {
String[] prefixes = (s == null) ? new String[] {} : s.split("[,\\s]+");
List buf = new ArrayList<>();
for (String prefix : prefixes) {
String tmp = prefix.trim();
if (tmp.length() > 0) {
buf.add(prefix);
}
}
Collections.sort(buf);
return buf;
}
private static String longestPrefixMatch(String path, String dflt) {
if (path == null || path.length() == 0) {
return dflt;
}
int length = 0;
String longest = null;
for (String prefix : ENDPOINT_PREFIXES) {
if (path.startsWith(prefix) && prefix.length() > length) {
longest = prefix;
length = prefix.length();
}
}
return (longest == null) ? dflt : longest;
}
/** Log a client request. */
public static void logClientRequest(HttpLogEntry entry) {
log(LOGGER, CLIENT, entry);
}
/**
* Log a client request.
* @deprecated Use {@link #logClientRequest(HttpLogEntry)} instead.
*/
@Deprecated
public static void logClientRequest(Logger logger, HttpLogEntry entry) {
log(logger, CLIENT, entry);
}
/** Log a request received by a server. */
public static void logServerRequest(HttpLogEntry entry) {
log(LOGGER, SERVER, entry);
}
/**
* Log a request received by a server.
* @deprecated Use {@link #logServerRequest(HttpLogEntry)} instead.
*/
@Deprecated
public static void logServerRequest(Logger logger, HttpLogEntry entry) {
log(logger, SERVER, entry);
}
private static void log(Logger logger, Marker marker, HttpLogEntry entry) {
Preconditions.checkNotNull(entry.method, "method");
Id dimensions = REGISTRY.createId("tags")
.withTag("mode", marker.getName())
.withTag("status", entry.getStatusTag())
.withTag("statusCode", entry.getStatusCodeTag())
.withTag("method", entry.method);
if (entry.clientName != null) {
dimensions = dimensions.withTag("client", entry.clientName);
}
if (marker == SERVER && entry.path != null) {
dimensions = dimensions.withTag("endpoint", longestPrefixMatch(entry.path, "other"));
}
// Update stats for the final attempt after retries are exhausted
if (!entry.canRetry || entry.attempt >= entry.maxAttempts) {
BucketTimer.get(REGISTRY, COMPLETE.withTags(dimensions.tags()), BUCKETS)
.record(entry.getOverallLatency(), TimeUnit.MILLISECONDS);
}
// Update stats for every actual http request
BucketTimer.get(REGISTRY, ATTEMPT.withTags(dimensions.tags()), BUCKETS)
.record(entry.getLatency(), TimeUnit.MILLISECONDS);
REGISTRY.distributionSummary(REQ_HEADER_SIZE.withTags(dimensions.tags()))
.record(entry.getRequestHeadersLength());
REGISTRY.distributionSummary(REQ_ENTITY_SIZE.withTags(dimensions.tags()))
.record(entry.requestContentLength);
REGISTRY.distributionSummary(RES_HEADER_SIZE.withTags(dimensions.tags()))
.record(entry.getResponseHeadersLength());
REGISTRY.distributionSummary(RES_ENTITY_SIZE.withTags(dimensions.tags()))
.record(entry.responseContentLength);
// Write data out to logger if enabled. For many monitoring use-cases there tend to be
// frequent requests that can be quite noisy so the log level is set to debug. This class is
// mostly intended to generate something like an access log so it presumes users who want the
// information will configure an appender based on the markers to send the data to a
// dedicated file. Others shouldn't have to deal with the spam in the logs, so debug for the
// level seems reasonable.
if (logger.isDebugEnabled(marker)) {
logger.debug(marker, entry.toString());
}
}
/** Generate a new request id. */
private static String newId() {
return UUID.randomUUID().toString();
}
// Cannot be static constant, date format is not thread-safe
private final SimpleDateFormat isoDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
private String clientName = null;
private String requestId = newId();
private String originalUri = null;
private String requestUri = null;
private String path = null;
private String method = null;
private List requestHeaders = new ArrayList<>();
private long requestContentLength = -1;
private String remoteAddr = null;
private int remotePort = -1;
private String attemptId = requestId;
private int attempt = 1;
private int maxAttempts = -1;
private boolean canRetry = false;
private int redirect = 0;
private Throwable exception = null;
private int statusCode = -1;
private String statusReason = null;
private List responseHeaders = new ArrayList<>();
private long responseContentLength = -1;
private List events = new ArrayList<>();
private long latency = -1;
private long originalStart = -1;
private void reset(int redir) {
if (originalStart < 0 && !events.isEmpty()) {
originalStart = events.get(0).timestamp();
}
requestHeaders.clear();
requestContentLength = -1;
remoteAddr = null;
remotePort = -1;
redirect = redir;
exception = null;
statusCode = -1;
responseHeaders.clear();
responseContentLength = -1;
events.clear();
latency = -1;
}
/** Set the name of the client, often used for clients to identify a particular config. */
public HttpLogEntry withClientName(String name) {
this.clientName = name;
return this;
}
/**
* Set the original uri. In the case of approaches with client-side load balancing this will
* be some alias indicating the group of hosts. The request uri would indicate a specific host
* used for an actual network request.
*/
public HttpLogEntry withOriginalUri(String uri) {
this.originalUri = uri;
return this;
}
/**
* Set the original uri. In the case of approaches with client-side load balancing this will
* be some alias indicating the group of hosts. The request uri would indicate a specific host
* used for an actual network request.
*/
public HttpLogEntry withOriginalUri(URI uri) {
return withOriginalUri(uri.toString());
}
/** Set the URI for the actual http request. */
public HttpLogEntry withRequestUri(String uri, String path) {
this.requestUri = uri;
this.path = path;
return this;
}
/** Set the URI for the actual http request. */
public HttpLogEntry withRequestUri(URI uri) {
return withRequestUri(uri.toString(), uri.getPath());
}
/** Set the method for the request. */
public HttpLogEntry withMethod(String httpMethod) {
this.method = httpMethod;
return this;
}
/** Add a header that was on the request. */
public HttpLogEntry withRequestHeader(String name, String value) {
requestHeaders.add(new Header(name, value));
return this;
}
/** Set the content-length for the request. */
public HttpLogEntry withRequestContentLength(long size) {
this.requestContentLength = size;
return this;
}
/**
* Set the remote address. For a client making a request this should be the server, for a
* server receiving a request it should be the client.
*/
public HttpLogEntry withRemoteAddr(String addr) {
this.remoteAddr = addr;
return this;
}
/**
* Set the remote port. For a client making a request this should be the server, for a
* server receiving a request it should be the client.
*/
public HttpLogEntry withRemotePort(int port) {
this.remotePort = port;
return this;
}
/** Set the attempt if retries are used, should only be used after the initial request. */
public HttpLogEntry withAttempt(int n) {
this.attempt = n;
this.attemptId = newId();
reset(0);
return this;
}
/** Set the attempt if redirect occurs, should only be used after the initial request. */
public HttpLogEntry withRedirect(URI loc) {
reset(redirect + 1);
return withRequestUri(loc);
}
/** Set the max number of attempts that will be tried. */
public HttpLogEntry withMaxAttempts(int attempts) {
this.maxAttempts = attempts;
return this;
}
/** Set to true if the error is one that can be retried. */
public HttpLogEntry withCanRetry(boolean retry) {
this.canRetry = retry;
return this;
}
/** Set the exception if there is a failure such as a connect timeout. */
public HttpLogEntry withException(Throwable t) {
exception = t;
return this;
}
/** Set the status code from the response. */
public HttpLogEntry withStatusCode(int code) {
this.statusCode = code;
return this;
}
/** Set the status reason from the response. */
public HttpLogEntry withStatusReason(String reason) {
this.statusReason = reason;
return this;
}
/** Add a header that was on the response. */
public HttpLogEntry withResponseHeader(String name, String value) {
responseHeaders.add(new Header(name, value));
return this;
}
/** Set the content-length from the response. */
public HttpLogEntry withResponseContentLength(long size) {
this.responseContentLength = size;
return this;
}
/** Set the latency for the request. */
public HttpLogEntry withRequestLatency(long t) {
this.latency = t;
return this;
}
/** Mark the time an event occurred. Should include at least the start and end of a request. */
public HttpLogEntry mark(String name) {
events.add(new Event(name, System.currentTimeMillis()));
return this;
}
/** Mark the time an event occurred. Should include at least the start and end of a request. */
public HttpLogEntry mark(String name, long timestamp) {
events.add(new Event(name, timestamp));
return this;
}
/** Return the request id. */
public String getRequestId() {
return requestId;
}
/** Return the attempt id. */
public String getAttemptId() {
return attemptId;
}
/**
* Return the latency for the request. If not explicitly set it will be calculated from the
* events.
*/
public long getLatency() {
if (latency >= 0L) {
return latency;
} else if (events.size() >= 2) {
return events.get(events.size() - 1).timestamp() - events.get(0).timestamp();
} else {
return -1;
}
}
/** Return the overall latency for a group of requests including all retries. */
public long getOverallLatency() {
if (maxAttempts <= 1 || originalStart < 0) {
return getLatency();
} else if (events.isEmpty()) {
return -1;
} else {
return events.get(events.size() - 1).timestamp() - originalStart;
}
}
/** Return the starting time for the request. */
public String getStartTime() {
return events.isEmpty()
? "unknown"
: isoDate.format(new Date(events.get(0).timestamp()));
}
private int getHeadersLength(List headers) {
int size = 0;
for (Header h : headers) {
size += h.numBytes();
}
return size;
}
/** Return the size in bytes of all request headers. */
public int getRequestHeadersLength() {
return getHeadersLength(requestHeaders);
}
/** Return the size in bytes of all response headers. */
public int getResponseHeadersLength() {
return getHeadersLength(responseHeaders);
}
/** Return a time line based on marked events. */
public String getTimeline() {
StringBuilder builder = new StringBuilder();
for (Event event : events) {
builder.append(event.name()).append(":").append(event.timestamp()).append(";");
}
return builder.toString();
}
private String getExceptionClass() {
return (exception == null)
? "null"
: exception.getClass().getName();
}
private String getExceptionMessage() {
return (exception == null)
? "null"
: exception.getMessage();
}
private String getHeaders(List headers) {
StringBuilder builder = new StringBuilder();
for (Header h : headers) {
builder.append(h.name()).append(':').append(h.value()).append(';');
}
return builder.toString();
}
/** Return a summary of all request headers. */
public String getRequestHeaders() {
return getHeaders(requestHeaders);
}
/** Return a summary of all response headers. */
public String getResponseHeaders() {
return getHeaders(responseHeaders);
}
private String getStatusTag() {
return (exception != null)
? exception.getClass().getSimpleName()
: (statusCode >= 100 ? (statusCode / 100) + "xx" : "unknown");
}
private String getStatusCodeTag() {
return (exception != null)
? exception.getClass().getSimpleName()
: (statusCode >= 100 ? "" + statusCode : "unknown");
}
@Override public String toString() {
return new StringBuilder()
.append(clientName).append('\t')
.append(getStartTime()).append('\t')
.append(getLatency()).append('\t')
.append(getOverallLatency()).append('\t')
.append(getTimeline()).append('\t')
.append(method).append('\t')
.append(originalUri).append('\t')
.append(requestUri).append('\t')
.append(remoteAddr).append('\t')
.append(remotePort).append('\t')
.append(statusCode).append('\t')
.append(statusReason).append('\t')
.append(getExceptionClass()).append('\t')
.append(getExceptionMessage()).append('\t')
.append(getRequestHeadersLength()).append('\t')
.append(requestContentLength).append('\t')
.append(getResponseHeadersLength()).append('\t')
.append(responseContentLength).append('\t')
.append(getRequestHeaders()).append('\t')
.append(getResponseHeaders()).append('\t')
.append(redirect).append('\t')
.append(attempt).append('\t')
.append(maxAttempts)
.toString();
}
private static class Header {
private final String name;
private final String value;
Header(String name, String value) {
this.name = name;
this.value = value;
}
String name() {
return name;
}
String value() {
return value;
}
int numBytes() {
return name.length() + ": ".length() + value.length() + "\n".length();
}
}
private static class Event {
private final String name;
private final long timestamp;
Event(String name, long timestamp) {
this.name = name;
this.timestamp = timestamp;
}
String name() {
return name;
}
long timestamp() {
return timestamp;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy