diff --git a/google-cloud-contrib/google-cloud-logging-logback/README.md b/google-cloud-contrib/google-cloud-logging-logback/README.md new file mode 100644 index 000000000000..e69b8edaecf0 --- /dev/null +++ b/google-cloud-contrib/google-cloud-logging-logback/README.md @@ -0,0 +1,135 @@ +Logback Appender for Google Cloud Logging +========================================= + +Logback appender `com.google.cloud.logging.logback.LoggingAppender` for +[Google Cloud Logging](https://cloud.google.com/logging/). + +This library allows you to use [Logback](https://logback.qos.ch/) configuration and the [Slf4j](https://www.slf4j.org/) interface to log via Google Cloud logging. + +- [Homepage](https://googlecloudplatform.github.io/google-cloud-java/) +- [API Documentation](https://googlecloudplatform.github.io/google-cloud-java/apidocs/index.html?com/google/cloud/logging/logback/LoggingAppender.html) + +> Note: This client is a work-in-progress, and may occasionally +> make backwards-incompatible changes. + +Quickstart +---------- +If you are using Maven, add this to your pom.xml file +```xml + + com.google.cloud + google-cloud-logging-logback + 0.18.0-alpha + +``` +If you are using Gradle, add this to your dependencies +```Groovy +compile 'com.google.cloud:google-cloud-logging-logback:0.18.0-alpha' +``` +If you are using SBT, add this to your dependencies +```Scala +libraryDependencies += "com.google.cloud" % "google-cloud-logging-logback" % "0.18.0-alpha" +``` + +Usage +----- + +Add the appender to your [Logback configuration](https://logback.qos.ch/manual/configuration.html) `logback.xml`. +See [Logback filters](https://logback.qos.ch/manual/filters.html#thresholdFilter) for information on filtering log output and + [encoders](https://logback.qos.ch/manual/encoders.html) for information on formatting. + + +```xml + + + + + INFO + + application.log + com.example.enhancers.TestLoggingEnhancer + com.example.enhancers.AnotherEnhancer + WARN + + + + + + +``` + +In your code : + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestLogger { + private final Logger logger = LoggerFactory.getLogger(TestLogger.class); + + public void log(String name) { + logger.info("This is a test"); + } + + public static void main(String[] args) { + TestLogger testLogger = new TestLogger(); + testLogger.log("test"); + } +} +``` + + +Authentication +-------------- + +See the [Authentication](https://github.com/GoogleCloudPlatform/google-cloud-java#authentication) +section in the base directory's README. + + +Limitations +----------- + +This library is usable, but not yet complete. +Open issues [here](https://github.com/GoogleCloudPlatform/google-cloud-java/issues?q=is%3Aissue+is%3Aopen+label%3A%22logging-logback%22). + + +Java Versions +------------- + +Java 7 or above is required for using this client. + +Versioning +---------- + +This library follows [Semantic Versioning](http://semver.org/). + +It is currently in major version zero (``0.y.z``), which means that anything +may change at any time and the public API should not be considered +stable. + +Contributing +------------ + +Contributions to this library are always welcome and highly encouraged. + +See `google-cloud`'s [CONTRIBUTING] documentation and the +[shared documentation](https://github.com/GoogleCloudPlatform/gcloud-common/blob/master/contributing/readme.md#how-to-contribute-to-gcloud) +for more information on how to get started. + +Please note that this project is released with a Contributor Code of Conduct. +By participating in this project you agree to abide by its terms. See +[Code of Conduct][code-of-conduct] for more information. + +License +------- +Apache 2.0 - See [LICENSE] for more information. + + +[CONTRIBUTING]:https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/CONTRIBUTING.md +[code-of-conduct]:https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/CODE_OF_CONDUCT.md#contributor-code-of-conduct +[LICENSE]: https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/LICENSE +[TESTING]: https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/TESTING.md#testing-code-that-uses-storage +[cloud-platform]: https://cloud.google.com/ + +[cloud-logging]: https://cloud.google.com/logging/ +[cloud-logging-docs]: https://cloud.google.com/logging/docs/overview diff --git a/google-cloud-contrib/google-cloud-logging-logback/pom.xml b/google-cloud-contrib/google-cloud-logging-logback/pom.xml new file mode 100644 index 000000000000..1c99617f1af0 --- /dev/null +++ b/google-cloud-contrib/google-cloud-logging-logback/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + google-cloud-logging-logback + 0.18.1-alpha-SNAPSHOT + jar + Google Cloud Logging Logback Appender + https://github.com/GoogleCloudPlatform/google-cloud-java/tree/master/google-cloud-contrib/google-cloud-logging-logback + + Logback support for Google Cloud Logging + + + + 1.2.3 + + + + com.google.cloud + google-cloud-contrib + 0.18.1-alpha-SNAPSHOT + + + + ch.qos.logback + logback-classic + ${logback.version} + + + com.google.cloud + google-cloud-logging + + + + org.easymock + easymock + 3.4 + test + + + junit + junit + 4.12 + test + + + diff --git a/google-cloud-contrib/google-cloud-logging-logback/src/main/java/com/google/cloud/logging/logback/LoggingAppender.java b/google-cloud-contrib/google-cloud-logging-logback/src/main/java/com/google/cloud/logging/logback/LoggingAppender.java new file mode 100644 index 000000000000..4f6a38f1b081 --- /dev/null +++ b/google-cloud-contrib/google-cloud-logging-logback/src/main/java/com/google/cloud/logging/logback/LoggingAppender.java @@ -0,0 +1,255 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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.logback; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.UnsynchronizedAppenderBase; +import com.google.cloud.MonitoredResource; +import com.google.cloud.logging.LogEntry; +import com.google.cloud.logging.Logging; +import com.google.cloud.logging.Logging.WriteOption; +import com.google.cloud.logging.LoggingEnhancer; +import com.google.cloud.logging.LoggingOptions; +import com.google.cloud.logging.MonitoredResourceUtil; +import com.google.cloud.logging.Payload; +import com.google.cloud.logging.Severity; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Logback appender for StackDriver Cloud Logging. + * + *

Appender configuration in logback.xml: + * + *

+ */ +public class LoggingAppender extends UnsynchronizedAppenderBase { + + private static final String LEVEL_NAME_KEY = "levelName"; + private static final String LEVEL_VALUE_KEY = "levelValue"; + + private volatile Logging logging; + private List loggingEnhancers; + private WriteOption[] defaultWriteOptions; + + private Level flushLevel; + private String log; + private String resourceType; + private Set enhancerClassNames = new HashSet<>(); + + /** + * Batched logging requests get immediately flushed for logs at or above this level. + * + *

Defaults to Error if not set. + * + * @param flushLevel Logback log level + */ + public void setFlushLevel(Level flushLevel) { + this.flushLevel = flushLevel; + } + + /** + * Sets the log filename. + * + * @param log filename + */ + public void setLog(String log) { + this.log = log; + } + + /** + * Sets the name of the monitored resource (Optional). + * + *

Must be a supported + * resource type. gae_app, gce_instance and container are auto-detected. + * + *

Defaults to "global" + * + * @param resourceType name of the monitored resource + */ + public void setResourceType(String resourceType) { + this.resourceType = resourceType; + } + + /** + * Add extra labels using classes that implement {@link LoggingEnhancer}. + */ + public void addEnhancer(String enhancerClassName) { + this.enhancerClassNames.add(enhancerClassName); + } + + Level getFlushLevel() { + return (flushLevel != null) ? flushLevel : Level.ERROR; + } + + String getLogName() { + return (log != null) ? log : "java.log"; + } + + MonitoredResource getMonitoredResource(String projectId) { + return MonitoredResourceUtil.getResource(projectId, resourceType); + } + + List getLoggingEnhancers() { + List loggingEnhancers = new ArrayList<>(); + if (enhancerClassNames != null) { + for (String enhancerClassName : enhancerClassNames) { + if (enhancerClassName != null) { + LoggingEnhancer enhancer = getEnhancer(enhancerClassName); + if (enhancer != null) { + loggingEnhancers.add(enhancer); + } + } + } + } + return loggingEnhancers; + } + + private LoggingEnhancer getEnhancer(String enhancerClassName) { + try { + Class clz = + (Class) + ClassLoader.getSystemClassLoader().loadClass(enhancerClassName.trim()); + return clz.newInstance(); + } catch (Exception ex) { + // If we cannot create the enhancer we fallback to null + } + return null; + } + + /** + * Initialize and configure the cloud logging service. + */ + @Override + public synchronized void start() { + if (isStarted()) { + return; + } + MonitoredResource resource = getMonitoredResource(getProjectId()); + defaultWriteOptions = + new WriteOption[]{WriteOption.logName(getLogName()), WriteOption.resource(resource)}; + getLogging().setFlushSeverity(severityFor(getFlushLevel())); + loggingEnhancers = new ArrayList<>(); + List resourceEnhancers = MonitoredResourceUtil.getResourceEnhancers(); + loggingEnhancers.addAll(resourceEnhancers); + loggingEnhancers.addAll(getLoggingEnhancers()); + super.start(); + } + + String getProjectId() { + return LoggingOptions.getDefaultInstance().getProjectId(); + } + + @Override + protected void append(ILoggingEvent e) { + LogEntry logEntry = logEntryFor(e); + getLogging().write(Collections.singleton(logEntry), defaultWriteOptions); + } + + @Override + public synchronized void stop() { + if (logging != null) { + try { + logging.close(); + } catch (Exception ex) { + // ignore + } + } + logging = null; + super.stop(); + } + + Logging getLogging() { + if (logging == null) { + synchronized (this) { + if (logging == null) { + logging = LoggingOptions.getDefaultInstance().getService(); + } + } + } + return logging; + } + + private LogEntry logEntryFor(ILoggingEvent e) { + String payload = e.getFormattedMessage(); + Level level = e.getLevel(); + LogEntry.Builder builder = + LogEntry.newBuilder(Payload.StringPayload.of(payload)) + .setTimestamp(e.getTimeStamp()) + .setSeverity(severityFor(level)); + + builder + .addLabel(LEVEL_NAME_KEY, level.toString()) + .addLabel(LEVEL_VALUE_KEY, String.valueOf(level.toInt())); + + if (loggingEnhancers != null) { + for (LoggingEnhancer enhancer : loggingEnhancers) { + enhancer.enhanceLogEntry(builder); + } + } + + return builder.build(); + } + + /** + * Transforms Logback logging levels to Cloud severity. + * + * @param level Logback logging level + * @return Cloud severity level + */ + private static Severity severityFor(Level level) { + switch (level.toInt()) { + // TRACE + case 5000: + return Severity.DEBUG; + // DEBUG + case 10000: + return Severity.DEBUG; + // INFO + case 20000: + return Severity.INFO; + // WARNING + case 30000: + return Severity.WARNING; + // ERROR + case 40000: + return Severity.ERROR; + default: + return Severity.DEFAULT; + } + } +} diff --git a/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/example/enhancers/AnotherTestLoggingEnhancer.java b/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/example/enhancers/AnotherTestLoggingEnhancer.java new file mode 100644 index 000000000000..77aed4e10666 --- /dev/null +++ b/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/example/enhancers/AnotherTestLoggingEnhancer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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.example.enhancers; + +import com.google.cloud.logging.LogEntry; +import com.google.cloud.logging.LoggingEnhancer; + +public class AnotherTestLoggingEnhancer implements LoggingEnhancer { + + @Override + public void enhanceLogEntry(LogEntry.Builder logEntry) { + logEntry.addLabel("test-label-2", "test-value-2"); + } +} diff --git a/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/example/enhancers/TestLoggingEnhancer.java b/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/example/enhancers/TestLoggingEnhancer.java new file mode 100644 index 000000000000..172e208761d0 --- /dev/null +++ b/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/example/enhancers/TestLoggingEnhancer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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.example.enhancers; + +import com.google.cloud.logging.LogEntry; +import com.google.cloud.logging.LoggingEnhancer; + +public class TestLoggingEnhancer implements LoggingEnhancer { + + @Override + public void enhanceLogEntry(LogEntry.Builder logEntry) { + logEntry.addLabel("test-label-1", "test-value-1"); + } +} diff --git a/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/google/cloud/logging/logback/LoggingAppenderTest.java b/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/google/cloud/logging/logback/LoggingAppenderTest.java new file mode 100644 index 000000000000..58142566dad1 --- /dev/null +++ b/google-cloud-contrib/google-cloud-logging-logback/src/test/java/com/google/cloud/logging/logback/LoggingAppenderTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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.logback; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.capture; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.filter.ThresholdFilter; +import ch.qos.logback.classic.spi.LoggingEvent; +import com.google.cloud.MonitoredResource; +import com.google.cloud.Timestamp; +import com.google.cloud.logging.LogEntry; +import com.google.cloud.logging.Logging; +import com.google.cloud.logging.Logging.WriteOption; +import com.google.cloud.logging.Payload.StringPayload; +import com.google.cloud.logging.Severity; +import com.google.common.collect.ImmutableMap; +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.easymock.EasyMockRunner; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(EasyMockRunner.class) +public class LoggingAppenderTest { + private final String projectId = "test-project"; + private final Logging logging = EasyMock.createStrictMock(Logging.class); + private LoggingAppender loggingAppender = new TestLoggingAppender(); + + class TestLoggingAppender extends LoggingAppender { + @Override + String getProjectId() { + return projectId; + } + + @Override + Logging getLogging() { + return logging; + } + } + + @Before + public void setUp() {} + + private final WriteOption[] defaultWriteOptions = + new WriteOption[] { + WriteOption.logName("java.log"), + WriteOption.resource( + MonitoredResource.newBuilder("global") + .setLabels( + new ImmutableMap.Builder().put("project_id", projectId).build()) + .build()) + }; + + @Test + public void testFlushLevelConfigUpdatesLoggingFlushSeverity() { + LogEntry logEntry = + LogEntry.newBuilder(StringPayload.of("this is a test")) + .setTimestamp(100000L) + .setSeverity(Severity.WARNING) + .setLabels( + new ImmutableMap.Builder() + .put("levelName", "WARN") + .put("levelValue", String.valueOf(30000L)) + .build()) + .build(); + logging.setFlushSeverity(Severity.WARNING); + Capture> capturedArgument = Capture.newInstance(); + logging.write( + capture(capturedArgument), (WriteOption) anyObject(), (WriteOption) anyObject()); + replay(logging); + Timestamp timestamp = Timestamp.ofTimeSecondsAndNanos(100000, 0); + LoggingEvent loggingEvent = createLoggingEvent(Level.WARN, timestamp.getSeconds()); + // error is the default, updating to warn for test + loggingAppender.setFlushLevel(Level.WARN); + loggingAppender.start(); + loggingAppender.doAppend(loggingEvent); + verify(logging); + Assert.assertTrue(capturedArgument.getValue().iterator().hasNext()); + Assert.assertTrue(logEntry.equals(capturedArgument.getValue().iterator().next())); + } + + @Test + public void testFilterLogsOnlyLogsAtOrAboveLogLevel() { + LogEntry logEntry = + LogEntry.newBuilder(StringPayload.of("this is a test")) + .setTimestamp(100000L) + .setSeverity(Severity.ERROR) + .setLabels( + new ImmutableMap.Builder() + .put("levelName", "ERROR") + .put("levelValue", String.valueOf(40000L)) + .build()) + .build(); + logging.setFlushSeverity(Severity.ERROR); + Capture> capturedArgument = Capture.newInstance(); + logging.write( + capture(capturedArgument), (WriteOption) anyObject(), (WriteOption) anyObject()); + expectLastCall().once(); + replay(logging); + Timestamp timestamp = Timestamp.ofTimeSecondsAndNanos(100000, 0); + LoggingEvent loggingEvent1 = createLoggingEvent(Level.INFO, timestamp.getSeconds()); + ThresholdFilter thresholdFilter = new ThresholdFilter(); + thresholdFilter.setLevel("ERROR"); + thresholdFilter.start(); + loggingAppender.addFilter(thresholdFilter); + loggingAppender.start(); + // info event does not get logged + loggingAppender.doAppend(loggingEvent1); + LoggingEvent loggingEvent2 = createLoggingEvent(Level.ERROR, timestamp.getSeconds()); + // error event gets logged + loggingAppender.doAppend(loggingEvent2); + verify(logging); + Assert.assertTrue(capturedArgument.getValue().iterator().hasNext()); + Assert.assertTrue(logEntry.equals(capturedArgument.getValue().iterator().next())); + } + + @Test + public void testEnhancersAddCorrectLabelsToLogEntries() { + LogEntry logEntry = + LogEntry.newBuilder(StringPayload.of("this is a test")) + .setTimestamp(100000L) + .setSeverity(Severity.WARNING) + .setLabels( + new ImmutableMap.Builder() + .put("levelName", "WARN") + .put("levelValue", String.valueOf(30000L)) + .put("test-label-1", "test-value-1") + .put("test-label-2", "test-value-2") + .build()) + .build(); + logging.setFlushSeverity(Severity.ERROR); + Capture> capturedArgument = Capture.newInstance(); + logging.write( + capture(capturedArgument), (WriteOption) anyObject(), (WriteOption) anyObject()); + expectLastCall().once(); + replay(logging); + loggingAppender.addEnhancer("com.example.enhancers.TestLoggingEnhancer"); + loggingAppender.addEnhancer("com.example.enhancers.AnotherTestLoggingEnhancer"); + loggingAppender.start(); + Timestamp timestamp = Timestamp.ofTimeSecondsAndNanos(100000, 0); + LoggingEvent loggingEvent = createLoggingEvent(Level.WARN, timestamp.getSeconds()); + loggingAppender.doAppend(loggingEvent); + verify(logging); + Assert.assertTrue(capturedArgument.getValue().iterator().hasNext()); + Assert.assertTrue(logEntry.equals(capturedArgument.getValue().iterator().next())); + } + + @Test + public void testDefaultWriteOptionsHasExpectedDefaults() { + logging.setFlushSeverity(Severity.ERROR); + Capture logNameArg = Capture.newInstance(); + Capture resourceArg = Capture.newInstance(); + logging.write((Iterable) anyObject(), capture(logNameArg), capture(resourceArg)); + expectLastCall().once(); + replay(logging); + loggingAppender.start(); + Timestamp timestamp = Timestamp.ofTimeSecondsAndNanos(100000, 0); + LoggingEvent loggingEvent = createLoggingEvent(Level.ERROR, timestamp.getSeconds()); + loggingAppender.doAppend(loggingEvent); + Assert.assertTrue(logNameArg.getValue().equals(defaultWriteOptions[0])); + Assert.assertTrue(resourceArg.getValue().equals(defaultWriteOptions[1])); + } + + private LoggingEvent createLoggingEvent(Level level, long timestamp) { + LoggingEvent loggingEvent = new LoggingEvent(); + loggingEvent.setMessage("this is a test"); + loggingEvent.setLevel(level); + loggingEvent.setTimeStamp(timestamp); + return loggingEvent; + } +} diff --git a/google-cloud-contrib/pom.xml b/google-cloud-contrib/pom.xml index b400ebc5f87e..341ff7125c69 100644 --- a/google-cloud-contrib/pom.xml +++ b/google-cloud-contrib/pom.xml @@ -20,6 +20,7 @@ google-cloud-nio google-cloud-nio-examples + google-cloud-logging-logback