Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement context handler to store HTTP request and tracing information #752

Merged
merged 12 commits into from Nov 23, 2021
@@ -0,0 +1,235 @@
/*
* Copyright 2021 Google LLC
*
* 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
*
* https://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.google.cloud.logging;

import com.google.cloud.logging.HttpRequest.RequestMethod;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import java.util.Objects;

/** Class to hold context attributes including information about {@see HttpRequest} and tracing. */
public class Context {
private final HttpRequest request;
private final String traceId;
private final String spanId;

/** A builder for {@see Context} objects. */
public static final class Builder {
private HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
private String traceId;
private String spanId;

Builder() {}

Builder(Context context) {
this.requestBuilder = context.request.toBuilder();
this.traceId = context.traceId;
this.spanId = context.spanId;
}

/** Sets the HTTP request. */
public Builder setRequest(HttpRequest request) {
this.requestBuilder = request.toBuilder();
return this;
}

public Builder setRequestUrl(String url) {
this.requestBuilder.setRequestUrl(url);
return this;
}

/** Sets the HTTP request method. */
public Builder setRequestMethod(RequestMethod method) {
this.requestBuilder.setRequestMethod(method);
return this;
}

/**
* Sets the referer URL of the request, as defined in HTTP/1.1 Header Field Definitions.
*
* @see <a href= "http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">HTTP/1.1 Header Field
* Definitions</a>
*/
public Builder setReferer(String referer) {
this.requestBuilder.setReferer(referer);
return this;
}

/**
* Sets the IP address (IPv4 or IPv6) of the client that issued the HTTP request. Examples:
* {@code 192.168.1.1}, {@code FE80::0202:B3FF:FE1E:8329}.
*/
public Builder setRemoteIp(String remoteIp) {
this.requestBuilder.setRemoteIp(remoteIp);
return this;
}

/**
* Sets the IP address (IPv4 or IPv6) of the origin server that the request was sent to.
* Examples: {@code 192.168.1.1}, {@code FE80::0202:B3FF:FE1E:8329}.
*/
public Builder setServerIp(String serverIp) {
this.requestBuilder.setServerIp(serverIp);
return this;
}

/** Sets the string as a trace id value. */
public Builder setTraceId(String traceId) {
this.traceId = traceId;
return this;
}

/** Sets the string as a span id value. */
public Builder setSpanId(String spanId) {
this.spanId = spanId;
return this;
}

/**
* Sets the trace id and span id values by parsing the string which represents xCloud Trace
* Context. The Cloud Trace Context is passed as {@code x-cloud-trace-context} header (can be in
* Pascal case format). The string format is <code>TRACE_ID/SPAN_ID;o=TRACE_TRUE</code>.
*
* @see <a href="https://cloud.google.com/trace/docs/setup#force-trace">Cloud Trace header
* format.</a>
*/
public Builder loadCloudTraceContext(String cloudTrace) {
if (cloudTrace != null) {
cloudTrace = cloudTrace.split(";")[0];
int split = cloudTrace.indexOf('/');
if (split >= 0) {
String traceId = cloudTrace.substring(0, split);
String spanId = cloudTrace.substring(split + 1);
if (!traceId.isEmpty()) {
setTraceId(traceId);
// do not set span Id without trace Id
if (!spanId.isEmpty()) {
setSpanId(spanId);
}
}
} else if (!cloudTrace.isEmpty()) {
setTraceId(cloudTrace);
}
}
return this;
}

/**
* Sets the trace id and span id values by parsing the string which represents the standard W3C
* trace context propagation header. The context propagation header is passed as {@code
* traceparent} header. The method currently supports ONLY version {@code "00"}. The string
* format is <code>00-TRACE_ID-SPAN_ID-FLAGS</code>. field of the {@code version-format} value.
*
* @see <a href=
* "https://www.w3.org/TR/trace-context/#traceparent-header-field-values">traceparent header
* value format</a>
* @throws IllegalArgumentException if passed argument does not follow the @W3C trace format or
* the format version is not supported.
*/
public Builder loadW3CTraceParentContext(String traceParent) throws IllegalArgumentException {
if (traceParent != null) {
String[] fields = traceParent.split("-");
if (fields.length > 3) {
String versionFormat = fields[0];
if (!versionFormat.equals("00")) {
throw new IllegalArgumentException("Not supporting versionFormat other than \"00\"");
}
} else {
throw new IllegalArgumentException(
"Invalid format of the header value. Expected \"00-traceid-spanid-arguments\"");
}
String traceId = fields[1];
if (!traceId.isEmpty()) {
setTraceId(traceId);
}
if (!Strings.isNullOrEmpty(traceId)) {
String spanId = fields[2];
if (!spanId.isEmpty()) {
setSpanId(spanId);
}
}
}
return this;
}

/** Creates a {@see Context} object for this builder. */
public Context build() {
return new Context(this);
}
}

Context(Builder builder) {
HttpRequest request = builder.requestBuilder.build();
if (!HttpRequest.EMPTY.equals(request)) {
this.request = request;
} else {
this.request = null;
}
this.traceId = builder.traceId;
this.spanId = builder.spanId;
}

public HttpRequest getHttpRequest() {
return this.request;
}

public String getTraceId() {
return this.traceId;
}

public String getSpanId() {
return this.spanId;
}

@Override
public int hashCode() {
return Objects.hash(request, traceId, spanId);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("request", request)
.add("traceId", traceId)
.add("spanId", spanId)
.toString();
}

@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Context)) {
return false;
}
Context other = (Context) obj;
return Objects.equals(request, other.request)
&& Objects.equals(traceId, other.traceId)
&& Objects.equals(spanId, other.spanId);
}

/** Returns a builder for this object. */
public Builder toBuilder() {
return new Builder(this);
}

/** Returns a builder for {@code HttpRequest} objects. */
public static Builder newBuilder() {
return new Builder();
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2021 Google LLC
*
* 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
*
* https://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.google.cloud.logging;

/** Class provides a per-thread storage of the {@see Context} instances. */
public class ContextHandler {
private static final ThreadLocal<Context> contextHolder = initContextHolder();

/**
* Initializes the context holder to {@link InheritableThreadLocal} if {@link LogManager}
* configuration property {@code com.google.cloud.logging.ContextHandler.useInheritedContext} is
* set to {@code true} or to {@link ThreadLocal} otherwise.
*
* @return instance of the context holder.
*/
private static ThreadLocal<Context> initContextHolder() {
LoggingConfig config = new LoggingConfig(ContextHandler.class.getName());
if (config.getUseInheritedContext()) {
return new InheritableThreadLocal<>();
} else {
return new ThreadLocal<>();
}
}

public Context getCurrentContext() {
Copy link
Contributor

@losalex losalex Nov 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should make it generic by turning this into template to be reused with other objects (not only Context). Seems that TraceLoggingEnhancer below also used ThreadLocal and might benefit from it as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should focus on supporting "our" context since this is what this feature is designed to be. Providing template means we include a generic-purpose implementation into logging library code.
If there will be a need for additional information for the context, the Context class can be extended to include additional data fields.

return contextHolder.get();
}

public void setCurrentContext(Context context) {
contextHolder.set(context);
}

public void removeCurrentContext() {
contextHolder.remove();
}
}
Expand Up @@ -36,6 +36,7 @@
public final class HttpRequest implements Serializable {

private static final long serialVersionUID = -274998005454709817L;
public static final HttpRequest EMPTY = newBuilder().build();

private final RequestMethod requestMethod;
private final String requestUrl;
Expand Down
Expand Up @@ -27,6 +27,7 @@
* parameter in https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry)
*/
public final class LogDestinationName extends Option {
private static final long serialVersionUID = 7944256748441111191L;

enum DestinationType implements Option.OptionType {
PROJECT,
Expand Down
Expand Up @@ -40,6 +40,7 @@ class LoggingConfig {
private static final String SYNCHRONICITY_TAG = "synchronicity";
private static final String RESOURCE_TYPE_TAG = "resourceType";
private static final String ENHANCERS_TAG = "enhancers";
private static final String USE_INHERITED_CONTEXT = "useInheritedContext";

public LoggingConfig(String className) {
this.className = className;
Expand Down Expand Up @@ -100,6 +101,18 @@ List<LoggingEnhancer> getEnhancers() {
return Collections.emptyList();
}

/**
* Returns boolean value of the property {@code
* com.google.cloud.logging.context.ContextHandler.useInheritedContext}. If no value is defined or
* the property does not represent a valid boolean value returns {@code false}.
*
* @return {@code true} or {@code false}
*/
boolean getUseInheritedContext() {
String flag = getProperty(USE_INHERITED_CONTEXT, "FALSE");
return Boolean.parseBoolean(flag);
}

private String getProperty(String name, String defaultValue) {
return firstNonNull(getProperty(name), defaultValue);
}
Expand All @@ -121,7 +134,7 @@ private Filter getFilterProperty(String name, Filter defaultValue) {
String stringFilter = getProperty(name);
try {
if (stringFilter != null) {
Class clz = ClassLoader.getSystemClassLoader().loadClass(stringFilter);
Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(stringFilter);
return (Filter) clz.getDeclaredConstructor().newInstance();
}
} catch (Exception ex) {
Expand All @@ -134,7 +147,7 @@ private Formatter getFormatterProperty(String name, Formatter defaultValue) {
String stringFilter = getProperty(name);
try {
if (stringFilter != null) {
Class clz = ClassLoader.getSystemClassLoader().loadClass(stringFilter);
Class<?> clz = ClassLoader.getSystemClassLoader().loadClass(stringFilter);
minherz marked this conversation as resolved.
Show resolved Hide resolved
return (Formatter) clz.getDeclaredConstructor().newInstance();
}
} catch (Exception ex) {
Expand Down
Expand Up @@ -200,6 +200,7 @@ public ApiFuture<AsyncPage<Sink>> getNextPage() {
}

private static class LogNamePageFetcher extends BasePageFetcher<String> {
private static final long serialVersionUID = 5308841362690185583L;

LogNamePageFetcher(
LoggingOptions serviceOptions, String cursor, Map<Option.OptionType, ?> requestOptions) {
Expand Down Expand Up @@ -244,6 +245,7 @@ public ApiFuture<AsyncPage<Metric>> getNextPage() {
}

private static class ExclusionPageFetcher extends BasePageFetcher<Exclusion> {
private static final long serialVersionUID = -1414118808031778916L;

ExclusionPageFetcher(
LoggingOptions serviceOptions, String cursor, Map<Option.OptionType, ?> requestOptions) {
Expand Down
Expand Up @@ -19,16 +19,9 @@
/* Adds tracing support for logging with thread-local trace ID tracking. */
public class TraceLoggingEnhancer implements LoggingEnhancer {

private static final String TRACE_ID = "trace_id";
private final String traceIdLabel;
public TraceLoggingEnhancer() {}

public TraceLoggingEnhancer() {
traceIdLabel = TRACE_ID;
}

public TraceLoggingEnhancer(String prefix) {
traceIdLabel = (prefix != null) ? prefix + TRACE_ID : TRACE_ID;
}
public TraceLoggingEnhancer(String prefix) {}

private static final ThreadLocal<String> traceId = new ThreadLocal<>();
minherz marked this conversation as resolved.
Show resolved Hide resolved

Expand Down