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: Add support for library instrumentation #979

Merged
merged 9 commits into from
Jun 25, 2022
Next Next commit
feat: Add support for library instrumentation
  • Loading branch information
losalex committed Jun 17, 2022
commit be250c30bfbec0da0983b335c7bd65c542b9a52d
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Copyright 2022 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
*
* 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.google.cloud.logging;

import com.google.api.client.util.Strings;
import com.google.api.gax.core.GaxProperties;
import com.google.cloud.Tuple;
import com.google.cloud.logging.Payload.JsonPayload;
import com.google.cloud.logging.Payload.Type;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.ListValue;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import java.util.ArrayList;
import java.util.List;

public class Instrumentation {
public static final String DIAGNOSTIC_INFO_KEY = "logging.googleapis.com/diagnostic";
public static final String INSTRUMENTATION_SOURCE_KEY = "instrumentation_source";
public static final String INSTRUMENTATION_NAME_KEY = "name";
public static final String INSTRUMENTATION_VERSION_KEY = "version";
public static final String JAVA_LIBRARY_NAME_PREFIX = "java";
public static final String DEFAULT_INSTRUMENTATION_VERSION = "UNKNOWN";
public static final int MAX_DIAGNOSTIC_VALUE_LENGTH = 14;
public static final int MAX_DIAGNOSTIC_ENTIES = 3;
private static boolean instrumentationAdded = false;

public static Tuple<Boolean, Iterable<LogEntry>> populateInstrumentationInfo(
losalex marked this conversation as resolved.
Show resolved Hide resolved
Iterable<LogEntry> logEntries) {
boolean isWritten = setInstrumentationStatus(true);
List<LogEntry> entries = new ArrayList<>();
boolean isInfoAdded = false;
for (LogEntry logEntry : logEntries) {
// Check if LogEntry has a proper payload and also contains a diagnostic entry
if (logEntry.getPayload().getType() == Type.JSON
&& logEntry
.<Payload.JsonPayload>getPayload()
.getData()
.containsFields(DIAGNOSTIC_INFO_KEY)) {
try {
ListValue infoList =
logEntry
.<Payload.JsonPayload>getPayload()
.getData()
.getFieldsOrThrow(DIAGNOSTIC_INFO_KEY)
.getStructValue()
.getFieldsOrThrow(INSTRUMENTATION_SOURCE_KEY)
.getListValue();
entries.add(createDiagnosticEntry(null, null, infoList));
isInfoAdded = isWritten = true;
} catch (Exception ex) {
System.err.println("ERROR: unexpected exception in populateInstrumentationInfo: " + ex);
}
} else {
entries.add(logEntry);
}
}
if (!isWritten) {
entries.add(createDiagnosticEntry(null, null, null));
isInfoAdded = true;
}
return Tuple.of(isInfoAdded, entries);
}

/**
* The helper method to generate a log entry with diagnostic instrumentation data.
*
* @param libraryName {string} The name of the logging library to be reported. Should be prefixed
* with 'java'. Will be truncated if longer than 14 characters.
* @param libraryVersion {string} The version of the logging library to be reported. Will be
* truncated if longer than 14 characters.
* @returns {LogEntry} The entry with diagnostic instrumentation data.
*/
public static LogEntry createDiagnosticEntry(String libraryName, String libraryVersion) {
return createDiagnosticEntry(libraryName, libraryVersion, null);
}

private static LogEntry createDiagnosticEntry(
String libraryName, String libraryVersion, ListValue existingLibraryList) {
losalex marked this conversation as resolved.
Show resolved Hide resolved
Struct instrumentation =
Struct.newBuilder()
.putAllFields(
ImmutableMap.of(
INSTRUMENTATION_SOURCE_KEY,
Value.newBuilder()
.setListValue(
generateLibrariesList(libraryName, libraryVersion, existingLibraryList))
.build()))
.build();
LogEntry entry =
LogEntry.of(
JsonPayload.of(
Struct.newBuilder()
.putAllFields(
ImmutableMap.of(
DIAGNOSTIC_INFO_KEY,
Value.newBuilder().setStructValue(instrumentation).build()))
.build()));
return entry;
}

private static ListValue generateLibrariesList(
String libraryName, String libraryVersion, ListValue existingLibraryList) {
if (Strings.isNullOrEmpty(libraryName) || !libraryName.startsWith(JAVA_LIBRARY_NAME_PREFIX))
losalex marked this conversation as resolved.
Show resolved Hide resolved
libraryName = JAVA_LIBRARY_NAME_PREFIX;
if (Strings.isNullOrEmpty(libraryVersion)) {
libraryVersion = GaxProperties.getLibraryVersion(Instrumentation.class.getClass());
if (Strings.isNullOrEmpty(libraryVersion)) libraryVersion = DEFAULT_INSTRUMENTATION_VERSION;
}
Struct libraryInfo = createInfoStruct(libraryName, libraryVersion);
ListValue.Builder libraryList = ListValue.newBuilder();
// Append first the library info for this library
libraryList.addValues(Value.newBuilder().setStructValue(libraryInfo).build());
if (existingLibraryList != null) {
for (Value val : existingLibraryList.getValuesList()) {
losalex marked this conversation as resolved.
Show resolved Hide resolved
if (val.hasStructValue()) {
try {
String name =
val.getStructValue().getFieldsOrThrow(INSTRUMENTATION_NAME_KEY).getStringValue();
if (Strings.isNullOrEmpty(name) || !name.startsWith(JAVA_LIBRARY_NAME_PREFIX)) continue;
String version =
val.getStructValue().getFieldsOrThrow(INSTRUMENTATION_VERSION_KEY).getStringValue();
if (Strings.isNullOrEmpty(version)) continue;
libraryList.addValues(
Value.newBuilder().setStructValue(createInfoStruct(name, version)).build());
if (libraryList.getValuesCount() == MAX_DIAGNOSTIC_ENTIES) break;
} catch (Exception ex) {
System.err.println("ERROR: unexpected exception in generateLibrariesList: " + ex);
losalex marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
return libraryList.build();
}

private static Struct createInfoStruct(String libraryName, String libraryVersion) {
return Struct.newBuilder()
.putAllFields(
ImmutableMap.of(
INSTRUMENTATION_NAME_KEY,
Value.newBuilder().setStringValue(truncateValue(libraryName)).build(),
INSTRUMENTATION_VERSION_KEY,
Value.newBuilder().setStringValue(truncateValue(libraryVersion)).build()))
.build();
}

/**
* The helper method used to set a status of a flag which indicates if instrumentation info
* already written or not.
*
* @param value {boolean} The value to be set.
* @returns The value of the flag before it is set.
*/
public static boolean setInstrumentationStatus(boolean value) {
losalex marked this conversation as resolved.
Show resolved Hide resolved
losalex marked this conversation as resolved.
Show resolved Hide resolved
if (instrumentationAdded == value) return instrumentationAdded;
return setAndGetInstrumentationStatus(value);
}

private static synchronized boolean setAndGetInstrumentationStatus(boolean value) {
boolean current = instrumentationAdded;
instrumentationAdded = value;
return current;
}

private static String truncateValue(String value) {
if (Strings.isNullOrEmpty(value) || value.length() < MAX_DIAGNOSTIC_VALUE_LENGTH) return value;
return value.substring(0, MAX_DIAGNOSTIC_VALUE_LENGTH) + "*";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ enum OptionType implements Option.OptionType {
RESOURCE,
LABELS,
LOG_DESTINATION,
AUTO_POPULATE_METADATA;
AUTO_POPULATE_METADATA,
PARTIAL_SUCCESS;

@SuppressWarnings("unchecked")
<T> T get(Map<Option.OptionType, ?> options) {
Expand Down Expand Up @@ -123,6 +124,15 @@ public static WriteOption destination(LogDestinationName destination) {
public static WriteOption autoPopulateMetadata(boolean autoPopulateMetadata) {
return new WriteOption(OptionType.AUTO_POPULATE_METADATA, autoPopulateMetadata);
}

/**
* Returns an option to set partialSuccess flag. See {@link
* https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write#body.request_body.FIELDS.partial_success}
* for more details.
*/
public static WriteOption partialSuccess(boolean partialSuccess) {
return new WriteOption(OptionType.PARTIAL_SUCCESS, partialSuccess);
}
}

/** Fields according to which log entries can be sorted. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;

import com.google.cloud.MonitoredResource;
import com.google.cloud.Tuple;
import com.google.cloud.logging.Logging.WriteOption;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
Expand Down Expand Up @@ -312,7 +313,9 @@ public void publish(LogRecord record) {
}
if (logEntry != null) {
try {
Iterable<LogEntry> logEntries = ImmutableList.of(logEntry);
Tuple<Boolean, Iterable<LogEntry>> pair =
Instrumentation.populateInstrumentationInfo(ImmutableList.of(logEntry));
Iterable<LogEntry> logEntries = pair.y();
if (autoPopulateMetadata) {
logEntries =
logging.populateMetadata(
Expand All @@ -321,7 +324,15 @@ public void publish(LogRecord record) {
if (redirectToStdout) {
logEntries.forEach(log -> System.out.println(log.toStructuredJsonString()));
} else {
logging.write(logEntries, defaultWriteOptions);
// Add partialSuccess option always for request containing instrumentation data
if (pair.x()) {
List<WriteOption> writeOptions = new ArrayList<WriteOption>();
writeOptions.addAll(Arrays.asList(defaultWriteOptions));
writeOptions.add(WriteOption.partialSuccess(true));
logging.write(logEntries, Iterables.toArray(writeOptions, WriteOption.class));
} else {
logging.write(logEntries, defaultWriteOptions);
}
losalex marked this conversation as resolved.
Show resolved Hide resolved
}
} catch (Exception ex) {
getErrorManager().error(null, ex, ErrorManager.WRITE_FAILURE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LABELS;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_DESTINATION;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_NAME;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.PARTIAL_SUCCESS;
import static com.google.cloud.logging.Logging.WriteOption.OptionType.RESOURCE;
import static com.google.common.base.Preconditions.checkNotNull;

Expand Down Expand Up @@ -92,7 +93,6 @@
import java.util.concurrent.TimeoutException;

class LoggingImpl extends BaseService<LoggingOptions> implements Logging {

protected static final String RESOURCE_NAME_FORMAT = "projects/%s/traces/%s";
private static final int FLUSH_WAIT_TIMEOUT_SECONDS = 6;
private final LoggingRpc rpc;
Expand Down Expand Up @@ -774,6 +774,7 @@ private static WriteLogEntriesRequest writeLogEntriesRequest(
builder.putAllLabels(labels);
}

builder.setPartialSuccess(Boolean.TRUE.equals(PARTIAL_SUCCESS.get(options)));
builder.addAllEntries(Iterables.transform(logEntries, LogEntry.toPbFunction(projectId)));
return builder.build();
}
Expand Down
Loading