From a2538e16123c21da62036b56df8c104360f1c2d6 Mon Sep 17 00:00:00 2001 From: Nicole Zhu <69952136+nicoleczhu@users.noreply.github.com> Date: Tue, 18 May 2021 18:02:21 -0700 Subject: [PATCH] fix(logging): correctly detect GKE resource (#4092) * fix(logging): gke resource autodetection * fix: detect gke resource correctly * chore: remove podname and namespace labels not supported * feat: detect podname from HOSTNAME envvar * feat: add resource.namespace_name * feat: add container name to resource.labels --- logging/logging.go | 202 --------------------------------- logging/resource.go | 268 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 202 deletions(-) create mode 100644 logging/resource.go diff --git a/logging/logging.go b/logging/logging.go index 02e05955651..81ede6afb0c 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -32,7 +32,6 @@ import ( "fmt" "log" "net/http" - "os" "regexp" "strconv" "strings" @@ -40,7 +39,6 @@ import ( "time" "unicode/utf8" - "cloud.google.com/go/compute/metadata" "cloud.google.com/go/internal/version" vkit "cloud.google.com/go/logging/apiv2" "cloud.google.com/go/logging/internal" @@ -242,206 +240,6 @@ type LoggerOption interface { set(*Logger) } -// 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.commonResource = r.MonitoredResource } - -var detectedResource struct { - pb *mrpb.MonitoredResource - once sync.Once -} - -func detectGCEResource() *mrpb.MonitoredResource { - projectID, err := metadata.ProjectID() - if err != nil { - return nil - } - id, err := metadata.InstanceID() - if err != nil { - return nil - } - zone, err := metadata.Zone() - if err != nil { - return nil - } - name, err := metadata.InstanceName() - if err != nil { - return nil - } - return &mrpb.MonitoredResource{ - Type: "gce_instance", - Labels: map[string]string{ - "project_id": projectID, - "instance_id": id, - "instance_name": name, - "zone": zone, - }, - } -} - -func isCloudRun() bool { - _, config := os.LookupEnv("K_CONFIGURATION") - _, service := os.LookupEnv("K_SERVICE") - _, revision := os.LookupEnv("K_REVISION") - return config && service && revision -} - -func detectCloudRunResource() *mrpb.MonitoredResource { - projectID, err := metadata.ProjectID() - if err != nil { - return nil - } - zone, err := metadata.Zone() - if err != nil { - return nil - } - return &mrpb.MonitoredResource{ - Type: "cloud_run_revision", - Labels: map[string]string{ - "project_id": projectID, - "location": regionFromZone(zone), - "service_name": os.Getenv("K_SERVICE"), - "revision_name": os.Getenv("K_REVISION"), - "configuration_name": os.Getenv("K_CONFIGURATION"), - }, - } -} - -func isCloudFunction() bool { - // Reserved envvars in older function runtimes, e.g. Node.js 8, Python 3.7 and Go 1.11. - _, name := os.LookupEnv("FUNCTION_NAME") - _, region := os.LookupEnv("FUNCTION_REGION") - _, entry := os.LookupEnv("ENTRY_POINT") - - // Reserved envvars in newer function runtimes. - _, target := os.LookupEnv("FUNCTION_TARGET") - _, signature := os.LookupEnv("FUNCTION_SIGNATURE_TYPE") - _, service := os.LookupEnv("K_SERVICE") - return (name && region && entry) || (target && signature && service) -} - -func detectCloudFunction() *mrpb.MonitoredResource { - projectID, err := metadata.ProjectID() - if err != nil { - return nil - } - zone, err := metadata.Zone() - if err != nil { - return nil - } - // Newer functions runtimes store name in K_SERVICE. - functionName, exists := os.LookupEnv("K_SERVICE") - if !exists { - functionName, _ = os.LookupEnv("FUNCTION_NAME") - } - return &mrpb.MonitoredResource{ - Type: "cloud_function", - Labels: map[string]string{ - "project_id": projectID, - "region": regionFromZone(zone), - "function_name": functionName, - }, - } -} - -// isAppEngine returns true for both standard and flex -func isAppEngine() bool { - _, service := os.LookupEnv("GAE_SERVICE") - _, version := os.LookupEnv("GAE_VERSION") - _, instance := os.LookupEnv("GAE_INSTANCE") - - return service && version && instance -} - -func detectAppEngineResource() *mrpb.MonitoredResource { - projectID, err := metadata.ProjectID() - if err != nil { - return nil - } - if projectID == "" { - projectID = os.Getenv("GOOGLE_CLOUD_PROJECT") - } - zone, err := metadata.Zone() - if err != nil { - return nil - } - - return &mrpb.MonitoredResource{ - Type: "gae_app", - Labels: map[string]string{ - "project_id": projectID, - "module_id": os.Getenv("GAE_SERVICE"), - "version_id": os.Getenv("GAE_VERSION"), - "instance_id": os.Getenv("GAE_INSTANCE"), - "runtime": os.Getenv("GAE_RUNTIME"), - "zone": zone, - }, - } -} - -func detectResource() *mrpb.MonitoredResource { - detectedResource.once.Do(func() { - switch { - // AppEngine, Functions, CloudRun are detected first, as metadata.OnGCE() - // erroneously returns true on these runtimes. - case isAppEngine(): - detectedResource.pb = detectAppEngineResource() - case isCloudFunction(): - detectedResource.pb = detectCloudFunction() - case isCloudRun(): - detectedResource.pb = detectCloudRunResource() - case metadata.OnGCE(): - detectedResource.pb = detectGCEResource() - } - }) - return detectedResource.pb -} - -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 regionFromZone(zone string) string { - cutoff := strings.LastIndex(zone, "-") - if cutoff > 0 { - return zone[:cutoff] - } - return zone -} - -func globalResource(projectID string) *mrpb.MonitoredResource { - return &mrpb.MonitoredResource{ - Type: "global", - Labels: map[string]string{ - "project_id": projectID, - }, - } -} - // CommonLabels are labels that apply to all log entries written from a Logger, // so that you don't have to repeat them in each log entry's Labels field. If // any of the log entries contains a (key, value) with the same key that is in diff --git a/logging/resource.go b/logging/resource.go new file mode 100644 index 00000000000..b73b289072a --- /dev/null +++ b/logging/resource.go @@ -0,0 +1,268 @@ +// 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 ( + "io/ioutil" + "os" + "strings" + "sync" + + "cloud.google.com/go/compute/metadata" + 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.commonResource = r.MonitoredResource } + +var detectedResource struct { + pb *mrpb.MonitoredResource + once sync.Once +} + +// isAppEngine returns true for both standard and flex +func isAppEngine() bool { + _, service := os.LookupEnv("GAE_SERVICE") + _, version := os.LookupEnv("GAE_VERSION") + _, instance := os.LookupEnv("GAE_INSTANCE") + + return service && version && instance +} + +func detectAppEngineResource() *mrpb.MonitoredResource { + projectID, err := metadata.ProjectID() + if err != nil { + return nil + } + if projectID == "" { + projectID = os.Getenv("GOOGLE_CLOUD_PROJECT") + } + zone, err := metadata.Zone() + if err != nil { + return nil + } + + return &mrpb.MonitoredResource{ + Type: "gae_app", + Labels: map[string]string{ + "project_id": projectID, + "module_id": os.Getenv("GAE_SERVICE"), + "version_id": os.Getenv("GAE_VERSION"), + "instance_id": os.Getenv("GAE_INSTANCE"), + "runtime": os.Getenv("GAE_RUNTIME"), + "zone": zone, + }, + } +} + +func isCloudFunction() bool { + // Reserved envvars in older function runtimes, e.g. Node.js 8, Python 3.7 and Go 1.11. + _, name := os.LookupEnv("FUNCTION_NAME") + _, region := os.LookupEnv("FUNCTION_REGION") + _, entry := os.LookupEnv("ENTRY_POINT") + + // Reserved envvars in newer function runtimes. + _, target := os.LookupEnv("FUNCTION_TARGET") + _, signature := os.LookupEnv("FUNCTION_SIGNATURE_TYPE") + _, service := os.LookupEnv("K_SERVICE") + return (name && region && entry) || (target && signature && service) +} + +func detectCloudFunction() *mrpb.MonitoredResource { + projectID, err := metadata.ProjectID() + if err != nil { + return nil + } + zone, err := metadata.Zone() + if err != nil { + return nil + } + // Newer functions runtimes store name in K_SERVICE. + functionName, exists := os.LookupEnv("K_SERVICE") + if !exists { + functionName, _ = os.LookupEnv("FUNCTION_NAME") + } + return &mrpb.MonitoredResource{ + Type: "cloud_function", + Labels: map[string]string{ + "project_id": projectID, + "region": regionFromZone(zone), + "function_name": functionName, + }, + } +} + +func isCloudRun() bool { + _, config := os.LookupEnv("K_CONFIGURATION") + _, service := os.LookupEnv("K_SERVICE") + _, revision := os.LookupEnv("K_REVISION") + return config && service && revision +} + +func detectCloudRunResource() *mrpb.MonitoredResource { + projectID, err := metadata.ProjectID() + if err != nil { + return nil + } + zone, err := metadata.Zone() + if err != nil { + return nil + } + return &mrpb.MonitoredResource{ + Type: "cloud_run_revision", + Labels: map[string]string{ + "project_id": projectID, + "location": regionFromZone(zone), + "service_name": os.Getenv("K_SERVICE"), + "revision_name": os.Getenv("K_REVISION"), + "configuration_name": os.Getenv("K_CONFIGURATION"), + }, + } +} + +func isKubernetesEngine() bool { + clusterName, err := metadata.InstanceAttributeValue("cluster-name") + // Note: InstanceAttributeValue can return "", nil + if err != nil || clusterName == "" { + return false + } + return true +} + +func detectKubernetesResource() *mrpb.MonitoredResource { + projectID, err := metadata.ProjectID() + if err != nil { + return nil + } + zone, err := metadata.Zone() + if err != nil { + return nil + } + clusterName, err := metadata.InstanceAttributeValue("cluster-name") + if err != nil { + return nil + } + namespaceBytes, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + namespaceName := "" + if err == nil { + namespaceName = string(namespaceBytes) + } + return &mrpb.MonitoredResource{ + Type: "k8s_container", + Labels: map[string]string{ + "cluster_name": clusterName, + "location": zone, + "project_id": projectID, + "pod_name": os.Getenv("HOSTNAME"), + "namespace_name": namespaceName, + // To get the `container_name` label, users need to explicitly provide it. + "container_name": os.Getenv("CONTAINER_NAME"), + }, + } +} + +func detectGCEResource() *mrpb.MonitoredResource { + projectID, err := metadata.ProjectID() + if err != nil { + return nil + } + id, err := metadata.InstanceID() + if err != nil { + return nil + } + zone, err := metadata.Zone() + if err != nil { + return nil + } + name, err := metadata.InstanceName() + if err != nil { + return nil + } + return &mrpb.MonitoredResource{ + Type: "gce_instance", + Labels: map[string]string{ + "project_id": projectID, + "instance_id": id, + "instance_name": name, + "zone": zone, + }, + } +} + +func detectResource() *mrpb.MonitoredResource { + detectedResource.once.Do(func() { + switch { + // AppEngine, Functions, CloudRun, Kubernetes are detected first, + // as metadata.OnGCE() erroneously returns true on these runtimes. + case isAppEngine(): + detectedResource.pb = detectAppEngineResource() + case isCloudFunction(): + detectedResource.pb = detectCloudFunction() + case isCloudRun(): + detectedResource.pb = detectCloudRunResource() + case isKubernetesEngine(): + detectedResource.pb = detectKubernetesResource() + case metadata.OnGCE(): + detectedResource.pb = detectGCEResource() + } + }) + return detectedResource.pb +} + +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 regionFromZone(zone string) string { + cutoff := strings.LastIndex(zone, "-") + if cutoff > 0 { + return zone[:cutoff] + } + return zone +} + +func globalResource(projectID string) *mrpb.MonitoredResource { + return &mrpb.MonitoredResource{ + Type: "global", + Labels: map[string]string{ + "project_id": projectID, + }, + } +}