diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/HttpRequest.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/HttpRequest.java index 1788d9669..2ba5e99dd 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/HttpRequest.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/HttpRequest.java @@ -69,7 +69,7 @@ public RequestMethod apply(String constant) { }; private static final StringEnumType type = - new StringEnumType(RequestMethod.class, CONSTRUCTOR); + new StringEnumType(RequestMethod.class, CONSTRUCTOR); public static final RequestMethod GET = type.createAndRegister("GET"); public static final RequestMethod HEAD = type.createAndRegister("HEAD"); diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java index 026838e7b..84779edd2 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LogEntry.java @@ -277,7 +277,7 @@ public Builder setSourceLocation(SourceLocation sourceLocation) { * * @see Log Entries and Logs */ - public Builder setPayload(Payload payload) { + public Builder setPayload(Payload payload) { this.payload = payload; return this; } @@ -434,7 +434,7 @@ public SourceLocation getSourceLocation() { * @see Log Entries and Logs */ @SuppressWarnings("unchecked") - public T getPayload() { + public > T getPayload() { return (T) payload; } diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java index 2607becde..7702161c9 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingHandler.java @@ -22,6 +22,7 @@ import com.google.cloud.logging.Logging.WriteOption; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.time.Instant; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -42,14 +43,38 @@ * Cloud Logging severities: * * - * - * - * - * - * - * - * - * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * *
Java LevelCloud Logging Severity
SEVEREERROR
WARNINGWARNING
INFOINFO
CONFIGINFO
FINEDEBUG
FINERDEBUG
FINESTDEBUG
Java LevelCloud Logging Severity
SEVEREERROR
WARNINGWARNING
INFOINFO
CONFIGINFO
FINEDEBUG
FINERDEBUG
FINESTDEBUG
* *

Original Java logging levels are added as labels (with {@code levelName} and {@code @@ -94,9 +119,6 @@ */ public class LoggingHandler extends Handler { - private static final String HANDLERS_PROPERTY = "handlers"; - private static final String ROOT_LOGGER_NAME = ""; - private static final String[] NO_HANDLERS = new String[0]; private static final String LEVEL_NAME_KEY = "levelName"; private static final String LEVEL_VALUE_KEY = "levelValue"; @@ -105,8 +127,10 @@ public class LoggingHandler extends Handler { private volatile Logging logging; - // Logs with the same severity with the base could be more efficiently sent to Cloud. - // Defaults to level of the handler or Level.FINEST if the handler is set to Level.ALL. + // Logs with the same severity with the base could be more efficiently sent to + // Cloud. + // Defaults to level of the handler or Level.FINEST if the handler is set to + // Level.ALL. // Currently there is no way to modify the base level, see // https://github.com/googleapis/google-cloud-java/issues/1740 . private final Level baseLevel; @@ -204,7 +228,8 @@ public LoggingHandler( this.enhancers.addAll(enhancersParam); - // In the following line getResourceEnhancers() never returns null (@NotNull attribute) + // In the following line getResourceEnhancers() never returns null (@NotNull + // attribute) List loggingEnhancers = MonitoredResourceUtil.getResourceEnhancers(); this.enhancers.addAll(loggingEnhancers); } catch (Exception ex) { @@ -219,8 +244,10 @@ public void publish(LogRecord record) { if (!isLoggable(record)) { return; } - // HACK warning: this logger doesn't work like normal loggers; the log calls are issued - // from another class instead of by itself, so it can't be configured off like normal + // HACK warning: this logger doesn't work like normal loggers; the log calls are + // issued + // from another class instead of by itself, so it can't be configured off like + // normal // loggers. We have to check the source class name instead. if ("io.netty.handler.codec.http2.Http2FrameLogger".equals(record.getSourceClassName())) { return; @@ -246,7 +273,7 @@ private LogEntry logEntryFor(LogRecord record) throws Exception { Level level = record.getLevel(); LogEntry.Builder builder = LogEntry.newBuilder(Payload.StringPayload.of(payload)) - .setTimestamp(record.getMillis()) + .setTimestamp(Instant.ofEpochMilli(record.getMillis())) .setSeverity(severityFor(level)); if (!baseLevel.equals(level)) { diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java index 402a498af..20c1eb29c 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/LoggingOptions.java @@ -98,6 +98,7 @@ protected LoggingOptions(Builder builder) { super(LoggingFactory.class, LoggingRpcFactory.class, builder, new LoggingDefaults()); } + @SuppressWarnings("serial") private static class LoggingDefaults implements ServiceDefaults { @Override diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java index a4bae228b..75517aa9c 100644 --- a/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/MonitoredResourceUtil.java @@ -16,9 +16,7 @@ package com.google.cloud.logging; -import com.google.cloud.MetadataConfig; import com.google.cloud.MonitoredResource; -import com.google.cloud.ServiceOptions; import com.google.cloud.logging.LogEntry.Builder; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMultimap; @@ -38,19 +36,27 @@ */ public class MonitoredResourceUtil { + private static final String APPENGINE_LABEL_PREFIX = "appengine.googleapis.com/"; + private static final String CLUSTER_NAME_ATTRIBUTE = "instance/attributes/cluster-name"; + private static final String K8S_POD_NAMESPACE_PATH = + "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; + private static final String FLEX_ENV = "flex"; + private static final String STD_ENV = "standard"; + private enum Label { - AppId("app_id"), ClusterName("cluster_name"), + ConfigurationName("configuration_name"), ContainerName("container_name"), + Env("env"), + FunctionName("function_name"), InstanceId("instance_id"), InstanceName("instance_name"), Location("location"), ModuleId("module_id"), - NamespaceId("namespace_id"), NamespaceName("namespace_name"), - PodId("pod_id"), PodName("pod_name"), ProjectId("project_id"), + Region("region"), RevisionName("revision_name"), ServiceName("service_name"), VersionId("version_id"), @@ -69,9 +75,8 @@ String getKey() { private enum Resource { CloudRun("cloud_run_revision"), - Container("container"), - GaeAppFlex("gae_app_flex"), - GaeAppStandard("gae_app_standard"), + CloudFunction("cloud_function"), + AppEngine("gae_app"), GceInstance("gce_instance"), K8sContainer("k8s_container"), Global("global"); @@ -87,21 +92,17 @@ String getKey() { } } - private static final String APPENGINE_LABEL_PREFIX = "appengine.googleapis.com/"; - private static ImmutableMultimap resourceTypeWithLabels = ImmutableMultimap.builder() + .putAll(Resource.CloudFunction.getKey(), Label.FunctionName, Label.Region) .putAll( - Resource.Container.getKey(), - Label.ClusterName, - Label.ContainerName, - Label.InstanceId, - Label.NamespaceId, - Label.PodId, - Label.Zone) - .putAll(Resource.CloudRun.getKey(), Label.RevisionName, Label.ServiceName, Label.Location) - .putAll(Resource.GaeAppFlex.getKey(), Label.ModuleId, Label.VersionId, Label.Zone) - .putAll(Resource.GaeAppStandard.getKey(), Label.ModuleId, Label.VersionId) + Resource.CloudRun.getKey(), + Label.RevisionName, + Label.ServiceName, + Label.Location, + Label.ConfigurationName) + .putAll( + Resource.AppEngine.getKey(), Label.ModuleId, Label.VersionId, Label.Zone, Label.Env) .putAll(Resource.GceInstance.getKey(), Label.InstanceId, Label.Zone) .putAll( Resource.K8sContainer.getKey(), @@ -112,22 +113,45 @@ String getKey() { Label.ContainerName) .build(); + private static Map cachedMonitoredResources = new HashMap<>(); + private static ResourceTypeEnvironmentGetter getter = new ResourceTypeEnvironmentGetterImpl(); + private MonitoredResourceUtil() {} - /* Return a self-configured monitored Resource. */ - public static MonitoredResource getResource(String projectId, String resourceTypeParam) { - String resourceType = resourceTypeParam; + /** + * Method is intended to assist in testing MonitoredResourceUtil class only. + * + * @param getter A mocked environment getter for simulated test environments. + */ + protected static void setEnvironmentGetter(ResourceTypeEnvironmentGetter getter) { + MonitoredResourceUtil.getter = getter; + } + + /** + * Build {@link MonitoredResource} based on detected resource type and populate it with labels + * following Monitored Resource Types documentation. + * + * @param projectId A string defining the project id + * @param resourceType A custom resource type + * @return the created {@link MonitoredResource} + * @see Monitored resource Types + */ + public static MonitoredResource getResource(String projectId, String resourceType) { + if (projectId == null || projectId.trim().isEmpty()) { + projectId = getter.getAttribute("project/project-id"); + } + + MonitoredResource result = cachedMonitoredResources.get(projectId + "/" + resourceType); + if (result != null) { + return result; + } + if (Strings.isNullOrEmpty(resourceType)) { - Resource detectedResourceType = getAutoDetectedResourceType(); + Resource detectedResourceType = detectResourceType(); resourceType = detectedResourceType.getKey(); } - // Currently, "gae_app" is the supported logging Resource type, but we distinguish - // between "gae_app_flex", "gae_app_standard" to support zone id, instance name logging on flex - // VMs. - // Hence, "gae_app_flex", "gae_app_standard" are trimmed to "gae_app" - String resourceName = resourceType.startsWith("gae_app") ? "gae_app" : resourceType; MonitoredResource.Builder builder = - MonitoredResource.newBuilder(resourceName).addLabel(Label.ProjectId.getKey(), projectId); + MonitoredResource.newBuilder(resourceType).addLabel(Label.ProjectId.getKey(), projectId); for (Label label : resourceTypeWithLabels.get(resourceType)) { String value = getValue(label, resourceType); @@ -135,7 +159,46 @@ public static MonitoredResource getResource(String projectId, String resourceTyp builder.addLabel(label.getKey(), value); } } - return builder.build(); + result = builder.build(); + cachedMonitoredResources.put(projectId + "/" + resourceType, result); + return result; + } + + /** + * Detect monitored Resource type using the following heuristic rules based on the environment and + * metadata server. + */ + private static Resource detectResourceType() { + // expects supported Google Cloud resource to have access to metadata server + if (getter.getAttribute("") == null) { + return Resource.Global; + } + + if (getter.getEnv("K_SERVICE") != null + && getter.getEnv("K_REVISION") != null + && getter.getEnv("K_CONFIGURATION") != null) { + return Resource.CloudRun; + } + if (getter.getEnv("GAE_INSTANCE") != null + && getter.getEnv("GAE_RUNTIME") != null + && getter.getEnv("GAE_SERVICE") != null + && getter.getEnv("GAE_VERSION") != null) { + return Resource.AppEngine; + } + if (getter.getEnv("FUNCTION_SIGNATURE_TYPE") != null + && getter.getEnv("FUNCTION_TARGET") != null) { + return Resource.CloudFunction; + } + if (getter.getAttribute(CLUSTER_NAME_ATTRIBUTE) != null) { + return Resource.K8sContainer; + } + if (getter.getAttribute("instance/preempted") != null + && getter.getAttribute("instance/cpu-platform") != null + && getter.getAttribute("instance/attributes/gae_app_bucket") == null) { + return Resource.GceInstance; + } + // other Google Cloud resources (e.g. CloudBuild) might be misdetected + return Resource.Global; } /** @@ -144,135 +207,152 @@ public static MonitoredResource getResource(String projectId, String resourceTyp * @return custom log entry enhancers */ public static List getResourceEnhancers() { - Resource resourceType = getAutoDetectedResourceType(); + Resource resourceType = detectResourceType(); return createEnhancers(resourceType); } + @SuppressWarnings("incomplete-switch") private static String getValue(Label label, String resourceType) { - String value; + String value = ""; + switch (label) { - case AppId: - value = ServiceOptions.getAppEngineAppId(); - break; case ClusterName: - value = MetadataConfig.getClusterName(); + value = getter.getAttribute(CLUSTER_NAME_ATTRIBUTE); + break; + case ConfigurationName: + value = getter.getEnv("K_CONFIGURATION"); break; case ContainerName: - if (resourceType.equals("k8s_container")) { - String hostName = System.getenv("HOSTNAME"); - value = hostName.substring(0, hostName.indexOf("-")); - } else { - value = MetadataConfig.getContainerName(); + // there is no determenistic way to discover name of container + // allow users to define the container name explicitly + value = getter.getEnv("CONTAINER_NAME"); + if (value == null) { + value = ""; + } + break; + case Env: + value = getAppEngineEnvironment(); + break; + case FunctionName: + case ServiceName: + value = getter.getEnv("K_SERVICE"); + if (value == null) { + value = getter.getEnv("FUNCTION_NAME"); } break; case InstanceId: - value = MetadataConfig.getInstanceId(); + value = getter.getAttribute("instance/id"); break; case InstanceName: - value = getAppEngineInstanceName(); + value = getter.getAttribute("instance/name"); break; case Location: - value = getCloudRunLocation(); + if (Resource.CloudFunction.getKey() == resourceType + || Resource.CloudRun.getKey() == resourceType) { + value = getRegion(); + } else { + value = getZone(); + } break; case ModuleId: - value = getAppEngineModuleId(); - break; - case NamespaceId: - value = MetadataConfig.getNamespaceId(); + value = getter.getEnv("GAE_SERVICE"); break; case NamespaceName: - String filePath = System.getenv("KUBERNETES_NAMESPACE_FILE"); - if (filePath == null) { - filePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; - } - try { - value = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new LoggingException(e, true); - } + value = getK8sNamespace(); break; case PodName: - case PodId: - value = System.getenv("HOSTNAME"); + // there is no determenistic way to discover name of container + // by default the pod name is set as pod's hostname + // note that hostname can be overriden in pod manifest or at runtime + value = getter.getEnv("HOSTNAME"); break; - case RevisionName: - value = System.getenv("K_REVISION"); + case Region: + value = getRegion(); break; - case ServiceName: - value = System.getenv("K_SERVICE"); + case RevisionName: + value = getter.getEnv("K_REVISION"); break; case VersionId: - value = getAppEngineVersionId(); + value = getter.getEnv("GAE_VERSION"); break; case Zone: - value = MetadataConfig.getZone(); - break; - default: - value = null; + value = getZone(); break; } + return value; } - /* Detect monitored Resource type using environment variables, else return global as default. */ - private static Resource getAutoDetectedResourceType() { - if (System.getenv("K_SERVICE") != null - && System.getenv("K_REVISION") != null - && System.getenv("K_CONFIGURATION") != null - && System.getenv("KUBERNETES_SERVICE_HOST") == null) { - return Resource.CloudRun; - } - if (System.getenv("GAE_INSTANCE") != null) { - return Resource.GaeAppFlex; - } - if (System.getenv("KUBERNETES_SERVICE_HOST") != null) { - return Resource.K8sContainer; - } - if (ServiceOptions.getAppEngineAppId() != null) { - return Resource.GaeAppStandard; - } - if (MetadataConfig.getInstanceId() != null) { - return Resource.GceInstance; + /** + * Heuristic to discover the namespace name of the current environment. There is no determenistic + * way to discover the namespace name of the process. The name is read from the {@link + * K8S_POD_NAMESPACE_PATH} when available or read from a user defined environment variable + * "NAMESPACE_NAME" + * + * @return Namespace name or empty string if the name could not be discovered + */ + private static String getK8sNamespace() { + String value = ""; + try { + value = + new String(Files.readAllBytes(Paths.get(K8S_POD_NAMESPACE_PATH)), StandardCharsets.UTF_8); + } catch (IOException e) { + // if SA token is not shared the info about namespace is unavailable + // allow users to define the namespace name explicitly + value = getter.getEnv("NAMESPACE_NAME"); + if (value == null) { + value = ""; + } } - // default Resource type - return Resource.Global; - } - - private static String getAppEngineModuleId() { - return System.getenv("GAE_SERVICE"); + return value; } - private static String getAppEngineVersionId() { - return System.getenv("GAE_VERSION"); + /** + * Distinguish between Standard and Flexible GAE environments. There is no indicator of the + * environment. The path to the startup-script in the metadata attribute was selected as one of + * the new values that explitly mentioning "flex" and cannot be altered by user (e.g. environment + * variable). The method assumes that the resource type is already identified as {@link + * Resource.AppEngine}. + * + * @return "flex" {@link String} for the Flexible environment and "standard" for the Standard. + */ + private static String getAppEngineEnvironment() { + String value = getter.getAttribute("instance/attributes/startup-script"); + if (value == "/var/lib/flex/startup_script.sh") { + return FLEX_ENV; + } + return STD_ENV; } - private static String getAppEngineInstanceName() { - return System.getenv("GAE_INSTANCE"); + /** + * Retrieves a region from the qualified region of 'projects/[PROJECT_NUMBER]/regions/[REGION]' + * + * @return region string id + */ + private static String getRegion() { + String loc = getter.getAttribute("instance/region"); + return loc.substring(loc.lastIndexOf('/') + 1); } - private static String getCloudRunLocation() { - String zone = MetadataConfig.getZone(); - // for Cloud Run managed, the zone is "REGION-1" - // So, we need to strip the "-1" to set location to just the region - if (zone.endsWith("-1")) return zone.substring(0, zone.length() - 2); - else return zone; + /** + * Retrieves a zone from the qualified zone of 'projects/[PROJECT_NUMBER]/zones/[ZONE]' + * + * @return zone string id + */ + private static String getZone() { + String loc = getter.getAttribute("instance/zone"); + return loc.substring(loc.lastIndexOf('/') + 1); } private static List createEnhancers(Resource resourceType) { List enhancers = new ArrayList<>(2); - switch (resourceType) { - // Trace logging enhancer is supported on GAE Flex and Standard. - case GaeAppFlex: + if (resourceType == Resource.AppEngine) { + enhancers.add(new TraceLoggingEnhancer(APPENGINE_LABEL_PREFIX)); + if (getAppEngineEnvironment() == FLEX_ENV) { enhancers.add( new LabelLoggingEnhancer( APPENGINE_LABEL_PREFIX, Collections.singletonList(Label.InstanceName))); - enhancers.add(new TraceLoggingEnhancer(APPENGINE_LABEL_PREFIX)); - break; - case GaeAppStandard: - enhancers.add(new TraceLoggingEnhancer(APPENGINE_LABEL_PREFIX)); - break; - default: - break; + } } return enhancers; } @@ -282,7 +362,7 @@ private static List createEnhancers(Resource resourceType) { * MonitoredResource.Builder#addLabel(String, String)} are restricted to a supported set per * resource. * - * @see Logging Labels + * @see Logging Labels */ private static class LabelLoggingEnhancer implements LoggingEnhancer { diff --git a/google-cloud-logging/src/main/java/com/google/cloud/logging/ResourceTypeEnvironmentGetter.java b/google-cloud-logging/src/main/java/com/google/cloud/logging/ResourceTypeEnvironmentGetter.java new file mode 100644 index 000000000..c77fa1988 --- /dev/null +++ b/google-cloud-logging/src/main/java/com/google/cloud/logging/ResourceTypeEnvironmentGetter.java @@ -0,0 +1,61 @@ +/* + * 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 + * + * 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.cloud.MetadataConfig; + +public interface ResourceTypeEnvironmentGetter { + + /** + * Gets the value of the specified environment variable. + * + * @param name the name of the environment variable + * @return the string value of the variable, or null if the variable is not defined + * in the system environment + * @see System#getenv() + */ + String getEnv(String name); + + /** + * Gets the value of the specified metadata server attribute. + * + * @param name the name of the metadata server attribute. + * @return the string value of the attribute, or null if the attribute is not defined + * in the metadata or the server is not available. + * @see MetadataConfig#getAttribute() + */ + String getAttribute(String name); +} + +final class ResourceTypeEnvironmentGetterImpl implements ResourceTypeEnvironmentGetter { + + @Override + public String getEnv(String name) { + // handle exception thrown if a security manager exists and blocks access to the + // process environment + try { + return System.getenv(name); + } catch (SecurityException ex) { + return null; + } + } + + @Override + public String getAttribute(String name) { + return MetadataConfig.getAttribute(name); + } +} diff --git a/google-cloud-logging/src/test/java/com/google/cloud/logging/MonitoredResourceUtilTest.java b/google-cloud-logging/src/test/java/com/google/cloud/logging/MonitoredResourceUtilTest.java new file mode 100644 index 000000000..ebcf398d9 --- /dev/null +++ b/google-cloud-logging/src/test/java/com/google/cloud/logging/MonitoredResourceUtilTest.java @@ -0,0 +1,281 @@ +/* + * 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 + * + * 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 static org.easymock.EasyMock.anyString; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.MonitoredResource; +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class MonitoredResourceUtilTest { + private static final String MOCKED_PROJECT_ID = "mocked-project-id"; + private static final String MOCKED_ZONE = "mocked-zone-id"; + private static final String MOCKED_QUALIFIED_ZONE = + "projects/" + MOCKED_PROJECT_ID + "/zones/" + MOCKED_ZONE; + private static final String MOCKED_REGION = "mocked-region-id"; + private static final String MOCKED_QUALIFIED_REGION = + "projects/" + MOCKED_PROJECT_ID + "/regions/" + MOCKED_REGION; + private static final String MOCKED_NON_EMPTY = "project/"; + + private ResourceTypeEnvironmentGetter getterMock; + + @Before + public void setup() { + getterMock = createMock(ResourceTypeEnvironmentGetter.class); + expect(getterMock.getAttribute("project/project-id")).andReturn(MOCKED_PROJECT_ID); + expect(getterMock.getAttribute("")).andReturn(MOCKED_NON_EMPTY).once(); + MonitoredResourceUtil.setEnvironmentGetter(getterMock); + } + + @After + public void teardown() { + verify(getterMock); + } + + @Test + public void testResourceTypeGlobal() { + final Map ExpectedLabels = + ImmutableMap.of("project_id", MonitoredResourceUtilTest.MOCKED_PROJECT_ID); + + // setup + expect(getterMock.getAttribute(anyString())).andReturn(null).anyTimes(); + expect(getterMock.getEnv(anyString())).andReturn(null).anyTimes(); + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource("", ""); + // verify + assertEquals(response.getType(), "global"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } + + public void testGetResourceWithParameters() { + final String MyProjectId = "my-project-id"; + final String MyResourceType = "my-resource-type"; + final Map ExpectedLabels = ImmutableMap.of("project_id", MyProjectId); + + // setup + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource(MyProjectId, MyResourceType); + // verify + assertEquals(response.getType(), "global"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } + + @Test + public void testResourceTypeGCEInstance() { + final String MockedInstanceId = "1234567890abcdefg"; + final Map ExpectedLabels = + ImmutableMap.of( + "project_id", + MonitoredResourceUtilTest.MOCKED_PROJECT_ID, + "instance_id", + MockedInstanceId, + "zone", + MOCKED_ZONE); + + // setup + expect(getterMock.getAttribute("instance/id")).andReturn(MockedInstanceId).anyTimes(); + expect(getterMock.getAttribute("instance/preempted")).andReturn(MOCKED_NON_EMPTY).once(); + expect(getterMock.getAttribute("instance/cpu-platform")).andReturn(MOCKED_NON_EMPTY).once(); + expect(getterMock.getAttribute("instance/zone")).andReturn(MOCKED_QUALIFIED_ZONE).once(); + expect(getterMock.getAttribute(anyString())).andReturn(null).anyTimes(); + expect(getterMock.getEnv(anyString())).andReturn(null).anyTimes(); + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource("", ""); + // verify + assertEquals(response.getType(), "gce_instance"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } + + /** + * Attention: This test can be flaky if the path + * /var/run/secrets/kubernetes.io/serviceaccount/namespace exists and the file at this path + * contains a value other than the mocked constant + */ + @Test + public void testResourceTypeK8sContainer() { + final String MockedClusterName = "mocked-cluster-1"; + final String MockedNamespaceName = "default"; + final String MockedPodName = "mocked-pod"; + final String MockedContainerName = "mocked-container"; + final Map ExpectedLabels = + ImmutableMap.builder() + .put("project_id", MonitoredResourceUtilTest.MOCKED_PROJECT_ID) + .put("cluster_name", MockedClusterName) + .put("location", MOCKED_ZONE) + .put("namespace_name", MockedNamespaceName) + .put("pod_name", MockedPodName) + .put("container_name", MockedContainerName) + .build(); + + // setup + expect(getterMock.getAttribute("instance/attributes/cluster-name")) + .andReturn(MockedClusterName) + .times(2); + expect(getterMock.getAttribute("instance/zone")).andReturn(MOCKED_QUALIFIED_ZONE).once(); + expect(getterMock.getAttribute(anyString())).andReturn(null).anyTimes(); + expect(getterMock.getEnv("HOSTNAME")).andReturn(MockedPodName).anyTimes(); + expect(getterMock.getEnv("NAMESPACE_NAME")).andReturn(MockedNamespaceName).anyTimes(); + expect(getterMock.getEnv("CONTAINER_NAME")).andReturn(MockedContainerName).anyTimes(); + expect(getterMock.getEnv(anyString())).andReturn(null).anyTimes(); + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource("", ""); + // verify + assertEquals(response.getType(), "k8s_container"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } + + private void setupCommonGAEMocks(String mockedModuleId, String mockedVersionId) { + expect(getterMock.getAttribute("instance/zone")).andReturn(MOCKED_QUALIFIED_ZONE).once(); + expect(getterMock.getEnv("GAE_INSTANCE")).andReturn(MOCKED_NON_EMPTY).once(); + expect(getterMock.getEnv("GAE_RUNTIME")).andReturn(MOCKED_NON_EMPTY).once(); + expect(getterMock.getEnv("GAE_SERVICE")).andReturn(mockedModuleId).times(2); + expect(getterMock.getEnv("GAE_VERSION")).andReturn(mockedVersionId).times(2); + expect(getterMock.getEnv(anyString())).andReturn(null).anyTimes(); + } + + @Test + public void testResourceTypeGAEStandardEnvironment() { + final String MockedModuleId = "mocked-module-id"; + final String MockedVersionId = "mocked-version-id"; + final Map ExpectedLabels = + ImmutableMap.of( + "project_id", + MonitoredResourceUtilTest.MOCKED_PROJECT_ID, + "module_id", + MockedModuleId, + "version_id", + MockedVersionId, + "env", + "standard", + "zone", + MOCKED_ZONE); + + // setup + setupCommonGAEMocks(MockedModuleId, MockedVersionId); + expect(getterMock.getAttribute(anyString())).andReturn(null).anyTimes(); + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource("", ""); + // verify + assertEquals(response.getType(), "gae_app"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } + + @Test + public void testResourceTypeGAEFlexibleEnvironment() { + final String MockedModuleId = "mocked-module-id"; + final String MockedVersionId = "mocked-version-id"; + final Map ExpectedLabels = + ImmutableMap.of( + "project_id", + MonitoredResourceUtilTest.MOCKED_PROJECT_ID, + "module_id", + MockedModuleId, + "version_id", + MockedVersionId, + "env", + "flex", + "zone", + MOCKED_ZONE); + + // setup + setupCommonGAEMocks(MockedModuleId, MockedVersionId); + expect(getterMock.getAttribute("instance/attributes/startup-script")) + .andReturn("/var/lib/flex/startup_script.sh") + .once(); + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource("", ""); + // verify + assertEquals(response.getType(), "gae_app"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } + + @Test + public void testResourceTypeCloudFunction() { + final String MockedFunctionName = "mocked-function-name"; + final Map ExpectedLabels = + ImmutableMap.of( + "project_id", + MonitoredResourceUtilTest.MOCKED_PROJECT_ID, + "function_name", + MockedFunctionName, + "region", + MOCKED_REGION); + + // setup + expect(getterMock.getAttribute("instance/region")).andReturn(MOCKED_QUALIFIED_REGION).once(); + expect(getterMock.getAttribute(anyString())).andReturn(null).anyTimes(); + expect(getterMock.getEnv("FUNCTION_SIGNATURE_TYPE")).andReturn(MOCKED_NON_EMPTY).once(); + expect(getterMock.getEnv("FUNCTION_TARGET")).andReturn(MOCKED_NON_EMPTY).once(); + expect(getterMock.getEnv("K_SERVICE")).andReturn(MockedFunctionName).anyTimes(); + expect(getterMock.getEnv(anyString())).andReturn(null).anyTimes(); + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource("", ""); + // verify + assertEquals(response.getType(), "cloud_function"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } + + @Test + public void testResourceTypeCloudRun() { + final String MockedRevisionName = "mocked-revision-name"; + final String MockedServiceName = "mocked-service-name"; + final String MockedConfigurationName = "mocked-config-name"; + final Map ExpectedLabels = + ImmutableMap.of( + "project_id", + MonitoredResourceUtilTest.MOCKED_PROJECT_ID, + "revision_name", + MockedRevisionName, + "configuration_name", + MockedConfigurationName, + "service_name", + MockedServiceName, + "location", + MOCKED_REGION); + + // setup + expect(getterMock.getAttribute("instance/region")).andReturn(MOCKED_QUALIFIED_REGION).once(); + expect(getterMock.getAttribute(anyString())).andReturn(null).anyTimes(); + expect(getterMock.getEnv("K_CONFIGURATION")).andReturn(MockedConfigurationName).times(2); + expect(getterMock.getEnv("K_REVISION")).andReturn(MockedRevisionName).times(2); + expect(getterMock.getEnv("K_SERVICE")).andReturn(MockedServiceName).anyTimes(); + expect(getterMock.getEnv(anyString())).andReturn(null).anyTimes(); + replay(getterMock); + // exercise + MonitoredResource response = MonitoredResourceUtil.getResource("", ""); + // verify + assertEquals(response.getType(), "cloud_run_revision"); + assertTrue(response.getLabels().equals(ExpectedLabels)); + } +}