File tree

9 files changed

+436
-6
lines changed

9 files changed

+436
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.logging;
18+
19+
import com.google.api.client.util.Strings;
20+
import com.google.api.gax.core.GaxProperties;
21+
import com.google.cloud.Tuple;
22+
import com.google.cloud.logging.Logging.WriteOption;
23+
import com.google.cloud.logging.Payload.JsonPayload;
24+
import com.google.cloud.logging.Payload.Type;
25+
import com.google.common.collect.ImmutableMap;
26+
import com.google.common.collect.Iterables;
27+
import com.google.protobuf.ListValue;
28+
import com.google.protobuf.Struct;
29+
import com.google.protobuf.Value;
30+
import java.util.ArrayList;
31+
import java.util.Arrays;
32+
import java.util.List;
33+
34+
public class Instrumentation {
35+
public static final String DIAGNOSTIC_INFO_KEY = "logging.googleapis.com/diagnostic";
36+
public static final String INSTRUMENTATION_SOURCE_KEY = "instrumentation_source";
37+
public static final String INSTRUMENTATION_NAME_KEY = "name";
38+
public static final String INSTRUMENTATION_VERSION_KEY = "version";
39+
public static final String JAVA_LIBRARY_NAME_PREFIX = "java";
40+
public static final String DEFAULT_INSTRUMENTATION_VERSION = "UNKNOWN";
41+
public static final String INSTRUMENTATION_LOG_NAME = "diagnostic-log";
42+
public static final int MAX_DIAGNOSTIC_VALUE_LENGTH = 14;
43+
public static final int MAX_DIAGNOSTIC_ENTIES = 3;
44+
private static boolean instrumentationAdded = false;
45+
private static Object instrumentationLock = new Object();
46+
47+
/**
48+
* Populates entries with instrumentation info which is added in separate log entry
49+
*
50+
* @param logEntries {Iterable<LogEntry>} The list of entries to be populated
51+
* @return {Tuple<Boolean, Iterable<LogEntry>>} containg a flag if instrumentation info was added
52+
* or not and a modified list of log entries
53+
*/
54+
public static Tuple<Boolean, Iterable<LogEntry>> populateInstrumentationInfo(
55+
Iterable<LogEntry> logEntries) {
56+
boolean isWritten = setInstrumentationStatus(true);
57+
if (isWritten) return Tuple.of(false, logEntries);
58+
List<LogEntry> entries = new ArrayList<>();
59+
60+
for (LogEntry logEntry : logEntries) {
61+
// Check if LogEntry has a proper payload and also contains a diagnostic entry
62+
if (!isWritten
63+
&& logEntry.getPayload().getType() == Type.JSON
64+
&& logEntry
65+
.<Payload.JsonPayload>getPayload()
66+
.getData()
67+
.containsFields(DIAGNOSTIC_INFO_KEY)) {
68+
try {
69+
ListValue infoList =
70+
logEntry
71+
.<Payload.JsonPayload>getPayload()
72+
.getData()
73+
.getFieldsOrThrow(DIAGNOSTIC_INFO_KEY)
74+
.getStructValue()
75+
.getFieldsOrThrow(INSTRUMENTATION_SOURCE_KEY)
76+
.getListValue();
77+
entries.add(createDiagnosticEntry(null, null, infoList));
78+
isWritten = true;
79+
} catch (Exception ex) {
80+
System.err.println("ERROR: unexpected exception in populateInstrumentationInfo: " + ex);
81+
}
82+
} else {
83+
entries.add(logEntry);
84+
}
85+
}
86+
if (!isWritten) {
87+
entries.add(createDiagnosticEntry(null, null, null));
88+
}
89+
return Tuple.of(true, entries);
90+
}
91+
92+
/**
93+
* Adds a partialSuccess flag option to array of WriteOption
94+
*
95+
* @param options {WriteOption[]} The options array to be extended
96+
* @return The new array of oprions containing WriteOption.OptionType.PARTIAL_SUCCESS flag set to
97+
* true
98+
*/
99+
public static WriteOption[] addPartialSuccessOption(WriteOption[] options) {
100+
if (options == null) return options;
101+
List<WriteOption> writeOptions = new ArrayList<WriteOption>();
102+
writeOptions.addAll(Arrays.asList(options));
103+
// Make sure we remove all partial success flags if any exist
104+
writeOptions.removeIf(
105+
option -> option.getOptionType() == WriteOption.OptionType.PARTIAL_SUCCESS);
106+
writeOptions.add(WriteOption.partialSuccess(true));
107+
return Iterables.toArray(writeOptions, WriteOption.class);
108+
}
109+
110+
/**
111+
* The helper method to generate a log entry with diagnostic instrumentation data.
112+
*
113+
* @param libraryName {string} The name of the logging library to be reported. Should be prefixed
114+
* with 'java'. Will be truncated if longer than 14 characters.
115+
* @param libraryVersion {string} The version of the logging library to be reported. Will be
116+
* truncated if longer than 14 characters.
117+
* @returns {LogEntry} The entry with diagnostic instrumentation data.
118+
*/
119+
public static LogEntry createDiagnosticEntry(String libraryName, String libraryVersion) {
120+
return createDiagnosticEntry(libraryName, libraryVersion, null);
121+
}
122+
123+
private static LogEntry createDiagnosticEntry(
124+
String libraryName, String libraryVersion, ListValue existingLibraryList) {
125+
Struct instrumentation =
126+
Struct.newBuilder()
127+
.putAllFields(
128+
ImmutableMap.of(
129+
INSTRUMENTATION_SOURCE_KEY,
130+
Value.newBuilder()
131+
.setListValue(
132+
generateLibrariesList(libraryName, libraryVersion, existingLibraryList))
133+
.build()))
134+
.build();
135+
LogEntry entry =
136+
LogEntry.newBuilder(
137+
JsonPayload.of(
138+
Struct.newBuilder()
139+
.putAllFields(
140+
ImmutableMap.of(
141+
DIAGNOSTIC_INFO_KEY,
142+
Value.newBuilder().setStructValue(instrumentation).build()))
143+
.build()))
144+
.setLogName(INSTRUMENTATION_LOG_NAME)
145+
.build();
146+
return entry;
147+
}
148+
149+
private static ListValue generateLibrariesList(
150+
String libraryName, String libraryVersion, ListValue existingLibraryList) {
151+
if (Strings.isNullOrEmpty(libraryName) || !libraryName.startsWith(JAVA_LIBRARY_NAME_PREFIX))
152+
libraryName = JAVA_LIBRARY_NAME_PREFIX;
153+
if (Strings.isNullOrEmpty(libraryVersion)) {
154+
libraryVersion = getLibraryVersion(Instrumentation.class.getClass());
155+
}
156+
Struct libraryInfo = createInfoStruct(libraryName, libraryVersion);
157+
ListValue.Builder libraryList = ListValue.newBuilder();
158+
// Append first the library info for this library
159+
libraryList.addValues(Value.newBuilder().setStructValue(libraryInfo).build());
160+
if (existingLibraryList != null) {
161+
for (Value val : existingLibraryList.getValuesList()) {
162+
if (val.hasStructValue()) {
163+
try {
164+
String name =
165+
val.getStructValue().getFieldsOrThrow(INSTRUMENTATION_NAME_KEY).getStringValue();
166+
if (Strings.isNullOrEmpty(name) || !name.startsWith(JAVA_LIBRARY_NAME_PREFIX)) continue;
167+
String version =
168+
val.getStructValue().getFieldsOrThrow(INSTRUMENTATION_VERSION_KEY).getStringValue();
169+
if (Strings.isNullOrEmpty(version)) continue;
170+
libraryList.addValues(
171+
Value.newBuilder().setStructValue(createInfoStruct(name, version)).build());
172+
if (libraryList.getValuesCount() == MAX_DIAGNOSTIC_ENTIES) break;
173+
} catch (Exception ex) {
174+
}
175+
}
176+
}
177+
}
178+
return libraryList.build();
179+
}
180+
181+
private static Struct createInfoStruct(String libraryName, String libraryVersion) {
182+
return Struct.newBuilder()
183+
.putAllFields(
184+
ImmutableMap.of(
185+
INSTRUMENTATION_NAME_KEY,
186+
Value.newBuilder().setStringValue(truncateValue(libraryName)).build(),
187+
INSTRUMENTATION_VERSION_KEY,
188+
Value.newBuilder().setStringValue(truncateValue(libraryVersion)).build()))
189+
.build();
190+
}
191+
192+
/**
193+
* The package-private helper method used to set the flag which indicates if instrumentation info
194+
* already written or not.
195+
*
196+
* @returns The value of the flag before it was set.
197+
*/
198+
static boolean setInstrumentationStatus(boolean value) {
199+
if (instrumentationAdded == value) return instrumentationAdded;
200+
synchronized (instrumentationLock) {
201+
boolean current = instrumentationAdded;
202+
instrumentationAdded = value;
203+
return current;
204+
}
205+
}
206+
207+
/**
208+
* Returns a library version associated with given class
209+
*
210+
* @param libraryClass {Class<?>} The class to be used to determine a library version
211+
* @return The version number string for given class or "UNKNOWN" if class library version cannot
212+
* be detected
213+
*/
214+
public static String getLibraryVersion(Class<?> libraryClass) {
215+
String libraryVersion = GaxProperties.getLibraryVersion(libraryClass);
216+
if (Strings.isNullOrEmpty(libraryVersion)) libraryVersion = DEFAULT_INSTRUMENTATION_VERSION;
217+
return libraryVersion;
218+
}
219+
220+
private static String truncateValue(String value) {
221+
if (Strings.isNullOrEmpty(value) || value.length() < MAX_DIAGNOSTIC_VALUE_LENGTH) return value;
222+
return value.substring(0, MAX_DIAGNOSTIC_VALUE_LENGTH) + "*";
223+
}
224+
}
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ enum OptionType implements Option.OptionType {
7171
RESOURCE,
7272
LABELS,
7373
LOG_DESTINATION,
74-
AUTO_POPULATE_METADATA;
74+
AUTO_POPULATE_METADATA,
75+
PARTIAL_SUCCESS;
7576

7677
@SuppressWarnings("unchecked")
7778
<T> T get(Map<Option.OptionType, ?> options) {
@@ -123,6 +124,15 @@ public static WriteOption destination(LogDestinationName destination) {
123124
public static WriteOption autoPopulateMetadata(boolean autoPopulateMetadata) {
124125
return new WriteOption(OptionType.AUTO_POPULATE_METADATA, autoPopulateMetadata);
125126
}
127+
128+
/**
129+
* Returns an option to set partialSuccess flag. See {@link
130+
* https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write#body.request_body.FIELDS.partial_success}
131+
* for more details.
132+
*/
133+
public static WriteOption partialSuccess(boolean partialSuccess) {
134+
return new WriteOption(OptionType.PARTIAL_SUCCESS, partialSuccess);
135+
}
126136
}
127137

128138
/** Fields according to which log entries can be sorted. */
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,10 @@ public void publish(LogRecord record) {
312312
}
313313
if (logEntry != null) {
314314
try {
315-
Iterable<LogEntry> logEntries = ImmutableList.of(logEntry);
315+
Iterable<LogEntry> logEntries =
316+
redirectToStdout
317+
? Instrumentation.populateInstrumentationInfo(ImmutableList.of(logEntry)).y()
318+
: ImmutableList.of(logEntry);
316319
if (autoPopulateMetadata) {
317320
logEntries =
318321
logging.populateMetadata(
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LABELS;
2424
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_DESTINATION;
2525
import static com.google.cloud.logging.Logging.WriteOption.OptionType.LOG_NAME;
26+
import static com.google.cloud.logging.Logging.WriteOption.OptionType.PARTIAL_SUCCESS;
2627
import static com.google.cloud.logging.Logging.WriteOption.OptionType.RESOURCE;
2728
import static com.google.common.base.Preconditions.checkNotNull;
2829

@@ -39,6 +40,7 @@
3940
import com.google.cloud.MonitoredResource;
4041
import com.google.cloud.MonitoredResourceDescriptor;
4142
import com.google.cloud.PageImpl;
43+
import com.google.cloud.Tuple;
4244
import com.google.cloud.logging.spi.v2.LoggingRpc;
4345
import com.google.common.annotations.VisibleForTesting;
4446
import com.google.common.base.Function;
@@ -92,7 +94,6 @@
9294
import java.util.concurrent.TimeoutException;
9395

9496
class LoggingImpl extends BaseService<LoggingOptions> implements Logging {
95-
9697
protected static final String RESOURCE_NAME_FORMAT = "projects/%s/traces/%s";
9798
private static final int FLUSH_WAIT_TIMEOUT_SECONDS = 6;
9899
private final LoggingRpc rpc;
@@ -774,6 +775,7 @@ private static WriteLogEntriesRequest writeLogEntriesRequest(
774775
builder.putAllLabels(labels);
775776
}
776777

778+
builder.setPartialSuccess(Boolean.TRUE.equals(PARTIAL_SUCCESS.get(options)));
777779
builder.addAllEntries(Iterables.transform(logEntries, LogEntry.toPbFunction(projectId)));
778780
return builder.build();
779781
}
@@ -851,15 +853,19 @@ public void write(Iterable<LogEntry> logEntries, WriteOption... options) {
851853
final Boolean logingOptionsPopulateFlag = getOptions().getAutoPopulateMetadata();
852854
final Boolean writeOptionPopulateFlga =
853855
WriteOption.OptionType.AUTO_POPULATE_METADATA.get(writeOptions);
856+
Tuple<Boolean, Iterable<LogEntry>> pair =
857+
Instrumentation.populateInstrumentationInfo(logEntries);
858+
logEntries = pair.y();
854859

855860
if (writeOptionPopulateFlga == Boolean.TRUE
856861
|| (writeOptionPopulateFlga == null && logingOptionsPopulateFlag == Boolean.TRUE)) {
857862
final MonitoredResource sharedResourceMetadata = RESOURCE.get(writeOptions);
858863
logEntries =
859864
populateMetadata(logEntries, sharedResourceMetadata, this.getClass().getName());
860865
}
861-
862-
writeLogEntries(logEntries, options);
866+
// Add partialSuccess option always for request containing instrumentation data
867+
writeLogEntries(
868+
logEntries, pair.x() ? Instrumentation.addPartialSuccessOption(options) : options);
863869
if (flushSeverity != null) {
864870
for (LogEntry logEntry : logEntries) {
865871
// flush pending writes if log severity at or above flush severity

0 commit comments

Comments
 (0)