Skip to content

Commit f1f8d85

Browse files
authored
feat: add support for metadata auto-population and redirection to STDOUT (#649)
Add configuration to opt-out metadata auto-population. Add configuration to redirect appender output to stdout. Add relevant unit tests. Refactor unit tests. Improve javadoc comments for methods. Remove warnings. Make methods with no modifier to be private.
1 parent 1c36ee1 commit f1f8d85

File tree

4 files changed

+332
-253
lines changed

4 files changed

+332
-253
lines changed

src/main/java/com/google/cloud/logging/logback/LoggingAppender.java

Lines changed: 135 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.google.common.collect.ImmutableList;
3939
import java.io.FileInputStream;
4040
import java.io.IOException;
41+
import java.time.Instant;
4142
import java.util.ArrayList;
4243
import java.util.Collections;
4344
import java.util.HashMap;
@@ -58,15 +59,21 @@
5859
* <level>INFO</level>
5960
* </filter>
6061
*
61-
* <!-- Optional: defaults to "java.log" -->
62+
* <!-- Optional: defaults to {@code "java.log"} -->
6263
* <log>application.log</log>
6364
*
64-
* <!-- Optional: defaults to "ERROR" -->
65+
* <!-- Optional: defaults to {@code "ERROR"} -->
6566
* <flushLevel>WARNING</flushLevel>
6667
*
67-
* <!-- Optional: defaults to ASYNC -->
68+
* <!-- Optional: defaults to {@code ASYNC} -->
6869
* <writeSynchronicity>SYNC</writeSynchronicity>
6970
*
71+
* <!-- Optional: defaults to {@code true} -->
72+
* <autoPopulateMetadata>false</autoPopulateMetadata>
73+
*
74+
* <!-- Optional: defaults to {@code false} -->
75+
* <redirectToStdout>true</redirectToStdout>
76+
*
7077
* &lt;!-- Optional: auto detects on App Engine Flex, Standard, GCE and GKE, defaults to "global". See <a
7178
* href=
7279
* "https://cloud.google.com/logging/docs/api/v2/resource-list">supported resource types</a> --&gt;
@@ -93,6 +100,7 @@ public class LoggingAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
93100

94101
private volatile Logging logging;
95102
private LoggingOptions loggingOptions;
103+
private MonitoredResource monitoredResource;
96104
private List<LoggingEnhancer> loggingEnhancers;
97105
private List<LoggingEventEnhancer> loggingEventEnhancers;
98106
private WriteOption[] defaultWriteOptions;
@@ -101,14 +109,16 @@ public class LoggingAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
101109
private String log;
102110
private String resourceType;
103111
private String credentialsFile;
112+
private boolean autoPopulateMetadata = true;
113+
private boolean redirectToStdout = false;
104114
private Synchronicity writeSyncFlag = Synchronicity.ASYNC;
105115
private final Set<String> enhancerClassNames = new HashSet<>();
106116
private final Set<String> loggingEventEnhancerClassNames = new HashSet<>();
107117

108118
/**
109-
* Batched logging requests get immediately flushed for logs at or above this level.
119+
* Sets a threshold for log severity level to flush all log entries that were batched so far.
110120
*
111-
* <p>Defaults to Error if not set.
121+
* <p>Defaults to Error.
112122
*
113123
* @param flushLevel Logback log level
114124
*/
@@ -117,49 +127,76 @@ public void setFlushLevel(Level flushLevel) {
117127
}
118128

119129
/**
120-
* Sets the log filename.
130+
* Sets the LOG_ID part of the <a href="log
131+
* name">https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.log_name</a>
132+
* for which the logs are ingested.
121133
*
122-
* @param log filename
134+
* @param log LOG_ID part of the name
123135
*/
124136
public void setLog(String log) {
125137
this.log = log;
126138
}
127139

128140
/**
129-
* Sets the name of the monitored resource (Optional).
130-
*
131-
* <p>Must be a <a href=
132-
* "https://cloud.google.com/logging/docs/api/v2/resource-list">supported</a> resource type.
133-
* gae_app, gce_instance and container are auto-detected.
141+
* Sets the name of the monitored resource (Optional). If not define the appender will try to
142+
* identify the resource type automatically. Currently support resource types include "gae_app",
143+
* "gce_instance", "k8s_container", "cloud_run_revision" and "cloud_function". If the appender
144+
* fails to identify the resource type it will be set to "global".
134145
*
135-
* <p>Defaults to "global"
146+
* <p>Must be a one of the <a href=
147+
* "https://cloud.google.com/logging/docs/api/v2/resource-list">supported</a> resource types.
136148
*
137-
* @param resourceType name of the monitored resource
149+
* @param resourceType the name of the monitored resource.
138150
*/
139151
public void setResourceType(String resourceType) {
140152
this.resourceType = resourceType;
141153
}
142154

143155
/**
144-
* Sets the credentials file to use to create the {@link LoggingOptions}. The credentials returned
145-
* by {@link GoogleCredentials#getApplicationDefault()} will be used if no custom credentials file
146-
* has been set.
156+
* Sets the path to the <a href="credential
157+
* file">https://cloud.google.com/iam/docs/creating-managing-service-account-keys</a>. If not set
158+
* the appender will use {@link GoogleCredentials#getApplicationDefault()} to authenticate.
147159
*
148-
* @param credentialsFile The credentials file to use.
160+
* @param credentialsFile the path to the credentials file.
149161
*/
150162
public void setCredentialsFile(String credentialsFile) {
151163
this.credentialsFile = credentialsFile;
152164
}
153165

154166
/**
155-
* Define synchronization mode for writing log entries.
167+
* Sets the log ingestion mode. It can be one of the {@link Synchronicity} values.
156168
*
157-
* @param flag to set {@code Synchronicity} value.
169+
* <p>Default to {@code Synchronicity.ASYNC}
170+
*
171+
* @param flag the new ingestion mode.
158172
*/
159173
public void setWriteSynchronicity(Synchronicity flag) {
160174
this.writeSyncFlag = flag;
161175
}
162176

177+
/**
178+
* Sets the automatic population of metadata fields for ingested logs.
179+
*
180+
* <p>Default to {@code true}.
181+
*
182+
* @param flag the metadata auto-population flag.
183+
*/
184+
public void setAutoPopulateMetadata(boolean flag) {
185+
autoPopulateMetadata = flag;
186+
}
187+
188+
/**
189+
* Sets the redirect of the appender's output to STDOUT instead of ingesting logs to Cloud Logging
190+
* using Logging API.
191+
*
192+
* <p>Default to {@code false}.
193+
*
194+
* @param flag the redirect flag.
195+
*/
196+
public void setRedirectToStdout(boolean flag) {
197+
redirectToStdout = flag;
198+
}
199+
163200
/** Add extra labels using classes that implement {@link LoggingEnhancer}. */
164201
public void addEnhancer(String enhancerClassName) {
165202
this.enhancerClassNames.add(enhancerClassName);
@@ -169,57 +206,70 @@ public void addLoggingEventEnhancer(String enhancerClassName) {
169206
this.loggingEventEnhancerClassNames.add(enhancerClassName);
170207
}
171208

172-
Level getFlushLevel() {
173-
return (flushLevel != null) ? flushLevel : Level.ERROR;
209+
/**
210+
* Returns the current value of the ingestion mode.
211+
*
212+
* <p>The method is deprecated. Use appender configuration to setup the ingestion
213+
*
214+
* @return a {@link Synchronicity} value of the ingestion module.
215+
*/
216+
@Deprecated
217+
public Synchronicity getWriteSynchronicity() {
218+
return (this.writeSyncFlag != null) ? this.writeSyncFlag : Synchronicity.ASYNC;
174219
}
175220

176-
String getLogName() {
177-
return (log != null) ? log : "java.log";
221+
private void setupMonitoredResource() {
222+
if (autoPopulateMetadata) {
223+
monitoredResource = MonitoredResourceUtil.getResource(getProjectId(), resourceType);
224+
} else {
225+
monitoredResource = null;
226+
}
178227
}
179228

180-
public Synchronicity getWriteSynchronicity() {
181-
return (this.writeSyncFlag != null) ? this.writeSyncFlag : Synchronicity.ASYNC;
229+
@InternalApi("Visible for testing")
230+
void setupMonitoredResource(MonitoredResource monitoredResource) {
231+
this.monitoredResource = monitoredResource;
182232
}
183233

184-
MonitoredResource getMonitoredResource(String projectId) {
185-
return MonitoredResourceUtil.getResource(projectId, resourceType);
234+
private Level getFlushLevel() {
235+
return (flushLevel != null) ? flushLevel : Level.ERROR;
186236
}
187237

188-
List<LoggingEnhancer> getLoggingEnhancers() {
189-
return getEnhancers(enhancerClassNames);
238+
private String getLogName() {
239+
return (log != null) ? log : "java.log";
190240
}
191241

192-
List<LoggingEventEnhancer> getLoggingEventEnhancers() {
242+
private List<LoggingEnhancer> getLoggingEnhancers() {
243+
return getEnhancers(enhancerClassNames, LoggingEnhancer.class);
244+
}
245+
246+
private List<LoggingEventEnhancer> getLoggingEventEnhancers() {
193247
if (loggingEventEnhancerClassNames.isEmpty()) {
194248
return DEFAULT_LOGGING_EVENT_ENHANCERS;
195249
} else {
196-
return getEnhancers(loggingEventEnhancerClassNames);
250+
return getEnhancers(loggingEventEnhancerClassNames, LoggingEventEnhancer.class);
197251
}
198252
}
199253

200-
<T> List<T> getEnhancers(Set<String> classNames) {
201-
List<T> loggingEnhancers = new ArrayList<>();
254+
private <T> List<T> getEnhancers(Set<String> classNames, Class<T> classOfT) {
255+
List<T> enhancers = new ArrayList<>();
202256
if (classNames != null) {
203-
for (String enhancerClassName : classNames) {
204-
if (enhancerClassName != null) {
205-
T enhancer = getEnhancer(enhancerClassName);
206-
if (enhancer != null) {
207-
loggingEnhancers.add(enhancer);
257+
for (String className : classNames) {
258+
if (className != null) {
259+
try {
260+
T enhancer =
261+
Loader.loadClass(className.trim())
262+
.asSubclass(classOfT)
263+
.getDeclaredConstructor()
264+
.newInstance();
265+
enhancers.add(enhancer);
266+
} catch (Exception ex) {
267+
// invalid className: ignore
208268
}
209269
}
210270
}
211271
}
212-
return loggingEnhancers;
213-
}
214-
215-
private <T> T getEnhancer(String enhancerClassName) {
216-
try {
217-
Class<T> clz = (Class<T>) Loader.loadClass(enhancerClassName.trim());
218-
return clz.getDeclaredConstructor().newInstance();
219-
} catch (Exception ex) {
220-
// If we cannot create the enhancer we fallback to null
221-
}
222-
return null;
272+
return enhancers;
223273
}
224274

225275
/** Initialize and configure the cloud logging service. */
@@ -228,9 +278,13 @@ public synchronized void start() {
228278
if (isStarted()) {
229279
return;
230280
}
231-
MonitoredResource resource = getMonitoredResource(getProjectId());
281+
282+
setupMonitoredResource();
283+
232284
defaultWriteOptions =
233-
new WriteOption[] {WriteOption.logName(getLogName()), WriteOption.resource(resource)};
285+
new WriteOption[] {
286+
WriteOption.logName(getLogName()), WriteOption.resource(monitoredResource)
287+
};
234288
Level flushLevel = getFlushLevel();
235289
if (flushLevel != Level.OFF) {
236290
getLogging().setFlushSeverity(severityFor(flushLevel));
@@ -251,8 +305,26 @@ String getProjectId() {
251305

252306
@Override
253307
protected void append(ILoggingEvent e) {
254-
LogEntry logEntry = logEntryFor(e);
255-
getLogging().write(Collections.singleton(logEntry), defaultWriteOptions);
308+
Iterable<LogEntry> entries = Collections.singleton(logEntryFor(e));
309+
if (autoPopulateMetadata) {
310+
entries =
311+
getLogging()
312+
.populateMetadata(
313+
entries,
314+
monitoredResource,
315+
"com.google.cloud.logging",
316+
"jdk",
317+
"sun",
318+
"java",
319+
"ch.qos.logback");
320+
}
321+
if (redirectToStdout) {
322+
for (LogEntry entry : entries) {
323+
System.out.println(entry.toStructuredJsonString());
324+
}
325+
} else {
326+
getLogging().write(entries, defaultWriteOptions);
327+
}
256328
}
257329

258330
@Override
@@ -281,6 +353,7 @@ Logging getLogging() {
281353
}
282354

283355
/** Flushes any pending asynchronous logging writes. */
356+
@Deprecated
284357
public void flush() {
285358
if (!isStarted()) {
286359
return;
@@ -293,15 +366,11 @@ public void flush() {
293366
/** Gets the {@link LoggingOptions} to use for this {@link LoggingAppender}. */
294367
protected LoggingOptions getLoggingOptions() {
295368
if (loggingOptions == null) {
296-
if (Strings.isNullOrEmpty(credentialsFile)) {
297-
loggingOptions = LoggingOptions.getDefaultInstance();
298-
} else {
369+
LoggingOptions.Builder builder = LoggingOptions.newBuilder();
370+
if (!Strings.isNullOrEmpty(credentialsFile)) {
299371
try {
300-
loggingOptions =
301-
LoggingOptions.newBuilder()
302-
.setCredentials(
303-
GoogleCredentials.fromStream(new FileInputStream(credentialsFile)))
304-
.build();
372+
builder.setCredentials(
373+
GoogleCredentials.fromStream(new FileInputStream(credentialsFile)));
305374
} catch (IOException e) {
306375
throw new RuntimeException(
307376
String.format(
@@ -310,6 +379,9 @@ protected LoggingOptions getLoggingOptions() {
310379
e);
311380
}
312381
}
382+
// opt-out metadata auto-population to control it in the appender code
383+
builder.setAutoPopulateMetadata(false);
384+
loggingOptions = builder.build();
313385
}
314386
return loggingOptions;
315387
}
@@ -328,7 +400,7 @@ private LogEntry logEntryFor(ILoggingEvent e) {
328400
}
329401
LogEntry.Builder builder =
330402
LogEntry.newBuilder(Payload.JsonPayload.of(jsonContent))
331-
.setTimestamp(e.getTimeStamp())
403+
.setTimestamp(Instant.ofEpochMilli(e.getTimeStamp()))
332404
.setSeverity(severity);
333405
builder
334406
.addLabel(LEVEL_NAME_KEY, level.toString())

src/test/java/com/example/enhancers/AnotherTestLoggingEnhancer.java

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)