| // 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 logging | |
| import ( | |
| "runtime" | |
| "strings" | |
| "sync" | |
| "cloud.google.com/go/logging/internal" | |
| mrpb "google.golang.org/genproto/googleapis/api/monitoredres" | |
| ) | |
| // CommonResource sets the monitored resource associated with all log entries | |
| // written from a Logger. If not provided, the resource is automatically | |
| // detected based on the running environment (on GCE, GCR, GCF and GAE Standard only). | |
| // This value can be overridden per-entry by setting an Entry's Resource field. | |
| func CommonResource(r *mrpb.MonitoredResource) LoggerOption { return commonResource{r} } | |
| type commonResource struct{ *mrpb.MonitoredResource } | |
| func (r commonResource) set(l *Logger) { | |
| l.commonResourceSet = true | |
| l.commonResource = r.MonitoredResource | |
| } | |
| type resource struct { | |
| pb *mrpb.MonitoredResource | |
| attrs internal.ResourceAttributesGetter | |
| once *sync.Once | |
| } | |
| var detectedResource = &resource{ | |
| attrs: internal.ResourceAttributes(), | |
| once: new(sync.Once), | |
| } | |
| func (r *resource) metadataProjectID() string { | |
| return r.attrs.Metadata("project/project-id") | |
| } | |
| func (r *resource) metadataZone() string { | |
| zone := r.attrs.Metadata("instance/zone") | |
| if zone != "" { | |
| return zone[strings.LastIndex(zone, "/")+1:] | |
| } | |
| return "" | |
| } | |
| func (r *resource) metadataRegion() string { | |
| region := r.attrs.Metadata("instance/region") | |
| if region != "" { | |
| return region[strings.LastIndex(region, "/")+1:] | |
| } | |
| return "" | |
| } | |
| // isMetadataActive queries valid response on "/computeMetadata/v1/" URL | |
| func (r *resource) isMetadataActive() bool { | |
| data := r.attrs.Metadata("") | |
| return data != "" | |
| } | |
| // isAppEngine returns true for both standard and flex | |
| func (r *resource) isAppEngine() bool { | |
| service := r.attrs.EnvVar("GAE_SERVICE") | |
| version := r.attrs.EnvVar("GAE_VERSION") | |
| instance := r.attrs.EnvVar("GAE_INSTANCE") | |
| return service != "" && version != "" && instance != "" | |
| } | |
| func detectAppEngineResource() *mrpb.MonitoredResource { | |
| projectID := detectedResource.metadataProjectID() | |
| if projectID == "" { | |
| projectID = detectedResource.attrs.EnvVar("GOOGLE_CLOUD_PROJECT") | |
| } | |
| if projectID == "" { | |
| return nil | |
| } | |
| zone := detectedResource.metadataZone() | |
| service := detectedResource.attrs.EnvVar("GAE_SERVICE") | |
| version := detectedResource.attrs.EnvVar("GAE_VERSION") | |
| return &mrpb.MonitoredResource{ | |
| Type: "gae_app", | |
| Labels: map[string]string{ | |
| "project_id": projectID, | |
| "module_id": service, | |
| "version_id": version, | |
| "zone": zone, | |
| }, | |
| } | |
| } | |
| func (r *resource) isCloudFunction() bool { | |
| target := r.attrs.EnvVar("FUNCTION_TARGET") | |
| signature := r.attrs.EnvVar("FUNCTION_SIGNATURE_TYPE") | |
| // note that this envvar is also present in Cloud Run environments | |
| service := r.attrs.EnvVar("K_SERVICE") | |
| return target != "" && signature != "" && service != "" | |
| } | |
| func detectCloudFunction() *mrpb.MonitoredResource { | |
| projectID := detectedResource.metadataProjectID() | |
| if projectID == "" { | |
| return nil | |
| } | |
| region := detectedResource.metadataRegion() | |
| functionName := detectedResource.attrs.EnvVar("K_SERVICE") | |
| return &mrpb.MonitoredResource{ | |
| Type: "cloud_function", | |
| Labels: map[string]string{ | |
| "project_id": projectID, | |
| "region": region, | |
| "function_name": functionName, | |
| }, | |
| } | |
| } | |
| func (r *resource) isCloudRunService() bool { | |
| config := r.attrs.EnvVar("K_CONFIGURATION") | |
| // note that this envvar is also present in Cloud Function environments | |
| service := r.attrs.EnvVar("K_SERVICE") | |
| revision := r.attrs.EnvVar("K_REVISION") | |
| return config != "" && service != "" && revision != "" | |
| } | |
| func (r *resource) isCloudRunJob() bool { | |
| if r.attrs.EnvVar("CLOUD_RUN_JOB") == "" { | |
| return false | |
| } | |
| if r.attrs.EnvVar("CLOUD_RUN_EXECUTION") == "" { | |
| return false | |
| } | |
| if r.attrs.EnvVar("CLOUD_RUN_TASK_INDEX") == "" { | |
| return false | |
| } | |
| if r.attrs.EnvVar("CLOUD_RUN_TASK_ATTEMPT") == "" { | |
| return false | |
| } | |
| return true | |
| } | |
| func detectCloudRunServiceResource() *mrpb.MonitoredResource { | |
| projectID := detectedResource.metadataProjectID() | |
| if projectID == "" { | |
| return nil | |
| } | |
| region := detectedResource.metadataRegion() | |
| config := detectedResource.attrs.EnvVar("K_CONFIGURATION") | |
| service := detectedResource.attrs.EnvVar("K_SERVICE") | |
| revision := detectedResource.attrs.EnvVar("K_REVISION") | |
| return &mrpb.MonitoredResource{ | |
| Type: "cloud_run_revision", | |
| Labels: map[string]string{ | |
| "project_id": projectID, | |
| "location": region, | |
| "service_name": service, | |
| "revision_name": revision, | |
| "configuration_name": config, | |
| }, | |
| } | |
| } | |
| func detectCloudRunJobResource() *mrpb.MonitoredResource { | |
| projectID := detectedResource.metadataProjectID() | |
| if projectID == "" { | |
| return nil | |
| } | |
| region := detectedResource.metadataRegion() | |
| job := detectedResource.attrs.EnvVar("CLOUD_RUN_JOB") | |
| return &mrpb.MonitoredResource{ | |
| Type: "cloud_run_job", | |
| Labels: map[string]string{ | |
| "project_id": projectID, | |
| "location": region, | |
| "job_name": job, | |
| }, | |
| } | |
| } | |
| func (r *resource) isKubernetesEngine() bool { | |
| clusterName := r.attrs.Metadata("instance/attributes/cluster-name") | |
| if clusterName == "" { | |
| return false | |
| } | |
| return true | |
| } | |
| func detectKubernetesResource() *mrpb.MonitoredResource { | |
| projectID := detectedResource.metadataProjectID() | |
| if projectID == "" { | |
| return nil | |
| } | |
| clusterName := detectedResource.attrs.Metadata("instance/attributes/cluster-name") | |
| clusterLocation := detectedResource.attrs.Metadata("instance/attributes/cluster-location") | |
| namespaceName := detectedResource.attrs.ReadAll("/var/run/secrets/kubernetes.io/serviceaccount/namespace") | |
| if namespaceName == "" { | |
| // if automountServiceAccountToken is disabled allow to customize | |
| // the namespace via environment | |
| namespaceName = detectedResource.attrs.EnvVar("NAMESPACE_NAME") | |
| } | |
| // note: if deployment customizes hostname, HOSTNAME envvar will have invalid content | |
| podName := detectedResource.attrs.EnvVar("HOSTNAME") | |
| // there is no way to derive container name from within container; use custom envvar if available | |
| containerName := detectedResource.attrs.EnvVar("CONTAINER_NAME") | |
| return &mrpb.MonitoredResource{ | |
| Type: "k8s_container", | |
| Labels: map[string]string{ | |
| "cluster_name": clusterName, | |
| "location": clusterLocation, | |
| "project_id": projectID, | |
| "pod_name": podName, | |
| "namespace_name": namespaceName, | |
| "container_name": containerName, | |
| }, | |
| } | |
| } | |
| func (r *resource) isComputeEngine() bool { | |
| preempted := r.attrs.Metadata("instance/preempted") | |
| platform := r.attrs.Metadata("instance/cpu-platform") | |
| appBucket := r.attrs.Metadata("instance/attributes/gae_app_bucket") | |
| return preempted != "" && platform != "" && appBucket == "" | |
| } | |
| func detectComputeEngineResource() *mrpb.MonitoredResource { | |
| projectID := detectedResource.metadataProjectID() | |
| if projectID == "" { | |
| return nil | |
| } | |
| id := detectedResource.attrs.Metadata("instance/id") | |
| zone := detectedResource.metadataZone() | |
| return &mrpb.MonitoredResource{ | |
| Type: "gce_instance", | |
| Labels: map[string]string{ | |
| "project_id": projectID, | |
| "instance_id": id, | |
| "zone": zone, | |
| }, | |
| } | |
| } | |
| func detectResource() *mrpb.MonitoredResource { | |
| detectedResource.once.Do(func() { | |
| if detectedResource.isMetadataActive() { | |
| name := systemProductName() | |
| switch { | |
| case name == "Google App Engine", detectedResource.isAppEngine(): | |
| detectedResource.pb = detectAppEngineResource() | |
| case name == "Google Cloud Functions", detectedResource.isCloudFunction(): | |
| detectedResource.pb = detectCloudFunction() | |
| // cannot use name validation for Cloud Run resources because | |
| // both of them set product name to "Google Cloud Run" | |
| case detectedResource.isCloudRunService(): | |
| detectedResource.pb = detectCloudRunServiceResource() | |
| case detectedResource.isCloudRunJob(): | |
| detectedResource.pb = detectCloudRunJobResource() | |
| // cannot use name validation for GKE and GCE because | |
| // both of them set product name to "Google Compute Engine" | |
| case detectedResource.isKubernetesEngine(): | |
| detectedResource.pb = detectKubernetesResource() | |
| case detectedResource.isComputeEngine(): | |
| detectedResource.pb = detectComputeEngineResource() | |
| } | |
| } | |
| }) | |
| return detectedResource.pb | |
| } | |
| // systemProductName reads resource type on the Linux-based environments such as | |
| // Cloud Functions, Cloud Run, GKE, GCE, GAE, etc. | |
| func systemProductName() string { | |
| if runtime.GOOS != "linux" { | |
| // We don't have any non-Linux clues available, at least yet. | |
| return "" | |
| } | |
| slurp := detectedResource.attrs.ReadAll("/sys/class/dmi/id/product_name") | |
| return strings.TrimSpace(slurp) | |
| } | |
| var resourceInfo = map[string]struct{ rtype, label string }{ | |
| "organizations": {"organization", "organization_id"}, | |
| "folders": {"folder", "folder_id"}, | |
| "projects": {"project", "project_id"}, | |
| "billingAccounts": {"billing_account", "account_id"}, | |
| } | |
| func monitoredResource(parent string) *mrpb.MonitoredResource { | |
| parts := strings.SplitN(parent, "/", 2) | |
| if len(parts) != 2 { | |
| return globalResource(parent) | |
| } | |
| info, ok := resourceInfo[parts[0]] | |
| if !ok { | |
| return globalResource(parts[1]) | |
| } | |
| return &mrpb.MonitoredResource{ | |
| Type: info.rtype, | |
| Labels: map[string]string{info.label: parts[1]}, | |
| } | |
| } | |
| func globalResource(projectID string) *mrpb.MonitoredResource { | |
| return &mrpb.MonitoredResource{ | |
| Type: "global", | |
| Labels: map[string]string{ | |
| "project_id": projectID, | |
| }, | |
| } | |
| } |