diff --git a/resource.go b/resource.go new file mode 100644 index 0000000..9576931 --- /dev/null +++ b/resource.go @@ -0,0 +1,109 @@ +// Copyright 2019, OpenCensus Authors +// +// 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 stackdriver // import "contrib.go.opencensus.io/exporter/stackdriver" + +import ( + "contrib.go.opencensus.io/resource/resourcekeys" + "go.opencensus.io/resource" + monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres" +) + +type resourceMap struct { + // Mapping from the input resource type to the monitored resource type in Stackdriver. + srcType, dstType string + // Mapping from Stackdriver monitored resource label to an OpenCensus resource label. + labels map[string]string +} + +// Resource labels that are generally internal to the exporter. +// Consider exposing these labels and a type identifier in the future to allow +// for customization. +const ( + stackdriverLocation = "contrib.opencensus.io/exporter/stackdriver/location" + stackdriverProjectID = "contrib.opencensus.io/exporter/stackdriver/project_id" + stackdriverGenericTaskNamespace = "contrib.opencensus.io/exporter/stackdriver/generic_task/namespace" + stackdriverGenericTaskJob = "contrib.opencensus.io/exporter/stackdriver/generic_task/job" + stackdriverGenericTaskID = "contrib.opencensus.io/exporter/stackdriver/generic_task/task_id" +) + +// Mappings for the well-known OpenCensus resources to applicable Stackdriver resources. +var resourceMappings = []resourceMap{ + { + srcType: resourcekeys.K8STypeContainer, + dstType: "k8s_container", + labels: map[string]string{ + "project_id": stackdriverProjectID, + "location": stackdriverLocation, + "cluster_name": resourcekeys.K8SKeyClusterName, + "namespace_name": resourcekeys.K8SKeyNamespaceName, + "pod_name": resourcekeys.K8SKeyPodName, + "container_name": resourcekeys.K8SKeyContainerName, + }, + }, + { + srcType: resourcekeys.GCPTypeGCEInstance, + dstType: "gce_instance", + labels: map[string]string{ + "project_id": resourcekeys.GCPKeyGCEProjectID, + "instance_id": resourcekeys.GCPKeyGCEInstanceID, + "zone": resourcekeys.GCPKeyGCEZone, + }, + }, + { + srcType: resourcekeys.AWSTypeEC2Instance, + dstType: "aws_ec2_instance", + labels: map[string]string{ + "project_id": stackdriverProjectID, + "instance_id": resourcekeys.AWSKeyEC2InstanceID, + "region": resourcekeys.AWSKeyEC2Region, + "aws_account": resourcekeys.AWSKeyEC2AccountID, + }, + }, + // Fallback to generic task resource. + { + srcType: "", + dstType: "generic_task", + labels: map[string]string{ + "project_id": stackdriverProjectID, + "location": stackdriverLocation, + "namespace": stackdriverGenericTaskNamespace, + "job": stackdriverGenericTaskJob, + "task_id": stackdriverGenericTaskID, + }, + }, +} + +func DefaultMapResource(res *resource.Resource) *monitoredrespb.MonitoredResource { +Outer: + for _, rm := range resourceMappings { + if res.Type != rm.srcType { + continue + } + result := &monitoredrespb.MonitoredResource{ + Type: rm.dstType, + Labels: make(map[string]string, len(rm.labels)), + } + for dst, src := range rm.labels { + if v, ok := res.Labels[src]; ok { + result.Labels[dst] = v + } else { + // A required label wasn't filled at all. Try subsequent mappings. + continue Outer + } + } + return result + } + return nil +} diff --git a/resource_test.go b/resource_test.go new file mode 100644 index 0000000..70bc4c5 --- /dev/null +++ b/resource_test.go @@ -0,0 +1,73 @@ +// Copyright 2019, OpenCensus Authors +// +// 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 stackdriver // import "contrib.go.opencensus.io/exporter/stackdriver" + +import ( + "fmt" + "testing" + + "contrib.go.opencensus.io/resource/resourcekeys" + "github.com/google/go-cmp/cmp" + "go.opencensus.io/resource" + monitoredrespb "google.golang.org/genproto/googleapis/api/monitoredres" +) + +func TestDefaultMapResource(t *testing.T) { + cases := []struct { + input *resource.Resource + want *monitoredrespb.MonitoredResource + }{ + // Verify that the mapping works and that we skip over the + // first mapping that doesn't apply. + { + input: &resource.Resource{ + Type: resourcekeys.GCPTypeGCEInstance, + Labels: map[string]string{ + resourcekeys.GCPKeyGCEProjectID: "proj1", + resourcekeys.GCPKeyGCEInstanceID: "inst1", + resourcekeys.GCPKeyGCEZone: "zone1", + "extra_key": "must be ignored", + }, + }, + want: &monitoredrespb.MonitoredResource{ + Type: "gce_instance", + Labels: map[string]string{ + "project_id": "proj1", + "instance_id": "inst1", + "zone": "zone1", + }, + }, + }, + // No match due to missing key. + { + input: &resource.Resource{ + Type: resourcekeys.GCPTypeGCEInstance, + Labels: map[string]string{ + resourcekeys.GCPKeyGCEProjectID: "proj1", + resourcekeys.GCPKeyGCEInstanceID: "inst1", + }, + }, + want: nil, + }, + } + for i, c := range cases { + t.Run(fmt.Sprintf("case-%d", i), func(t *testing.T) { + got := DefaultMapResource(c.input) + if diff := cmp.Diff(got, c.want); diff != "" { + t.Errorf("Values differ -got +want: %s", diff) + } + }) + } +} diff --git a/stackdriver.go b/stackdriver.go index 5953773..a6e7748 100644 --- a/stackdriver.go +++ b/stackdriver.go @@ -52,10 +52,14 @@ import ( "errors" "fmt" "log" + "os" + "path" "time" + metadataapi "cloud.google.com/go/compute/metadata" traceapi "cloud.google.com/go/trace/apiv2" "contrib.go.opencensus.io/exporter/stackdriver/monitoredresource" + "go.opencensus.io/resource" "go.opencensus.io/stats/view" "go.opencensus.io/tag" "go.opencensus.io/trace" @@ -73,9 +77,22 @@ type Options struct { // ProjectID is the identifier of the Stackdriver // project the user is uploading the stats data to. // If not set, this will default to your "Application Default Credentials". - // For details see: https://developers.google.com/accounts/docs/application-default-credentials + // For details see: https://developers.google.com/accounts/docs/application-default-credentials. + // + // It will be used in the project_id label of a Stackdriver monitored + // resource if the resource does not inherently belong to a specific + // project, e.g. on-premise resource like k8s_container or generic_task. ProjectID string + // Location is the identifier of the GCP or AWS cloud region/zone in which + // the data for a resource is stored. + // If not set, it will default to the location provided by the metadata server. + // + // It will be used in the location label of a Stackdriver monitored resource + // if the resource does not inherently belong to a specific project, e.g. + // on-premise resource like k8s_container or generic_task. + Location string + // OnError is the hook to be called when there is // an error uploading the stats or tracing data. // If no custom hook is set, errors are logged. @@ -153,6 +170,21 @@ type Options struct { // Optional, but encouraged. MonitoredResource monitoredresource.Interface + // ResourceDetector provides a hook to discover arbitrary resource information. + // + // The translation function provided in MapResource must be able to conver the + // the resource information to a Stackdriver monitored resource. + // + // If this field is unset, resource type and tags will automatically be discovered through + // the OC_RESOURCE_TYPE and OC_RESOURCE_LABELS environment variables. + ResourceDetector resource.Detector + + // MapResource converts a OpenCensus resource to a Stackdriver monitored resource. + // + // If this field is unset, DefaultMapResource will be used which encodes a set of default + // conversions from auto-detected resources to well-known Stackdriver monitored resources. + MapResource func(*resource.Resource) *monitoredrespb.MonitoredResource + // MetricPrefix overrides the prefix of a Stackdriver metric display names. // Optional. If unset defaults to "OpenCensus/". // Deprecated: Provide GetMetricDisplayName to change the display name of @@ -253,10 +285,45 @@ func NewExporter(o Options) (*Exporter, error) { } o.ProjectID = creds.ProjectID } + if o.Location == "" { + ctx := o.Context + if ctx == nil { + ctx = context.Background() + } + zone, err := metadataapi.Zone() + if err != nil { + log.Printf("Setting Stackdriver default location failed: %s", err) + } else { + log.Printf("Setting Stackdriver default location to %q", zone) + o.Location = zone + } + } if o.MonitoredResource != nil { o.Resource = convertMonitoredResourceToPB(o.MonitoredResource) } + if o.MapResource == nil { + o.MapResource = DefaultMapResource + } + if o.ResourceDetector != nil { + // For backwards-compatibility we still respect the deprecated resource field. + if o.Resource != nil { + return nil, errors.New("stackdriver: ResourceDetector must not be used in combination with deprecated resource fields") + } + res, err := o.ResourceDetector(o.Context) + if err != nil { + return nil, fmt.Errorf("stackdriver: detect resource: %s", err) + } + // Populate internal resource labels for defaulting project_id, location, and + // generic resource labels of applicable monitored resources. + res.Labels[stackdriverProjectID] = o.ProjectID + res.Labels[stackdriverLocation] = o.Location + res.Labels[stackdriverGenericTaskNamespace] = "default" + res.Labels[stackdriverGenericTaskJob] = path.Base(os.Args[0]) + res.Labels[stackdriverGenericTaskID] = getTaskValue() + + o.Resource = o.MapResource(res) + } se, err := newStatsExporter(o) if err != nil { diff --git a/stats.go b/stats.go index ca82ca7..4bde9cb 100644 --- a/stats.go +++ b/stats.go @@ -25,13 +25,13 @@ import ( "sync" "time" - "go.opencensus.io" + opencensus "go.opencensus.io" "go.opencensus.io/stats" "go.opencensus.io/stats/view" "go.opencensus.io/tag" "go.opencensus.io/trace" - "cloud.google.com/go/monitoring/apiv3" + monitoring "cloud.google.com/go/monitoring/apiv3" "github.com/golang/protobuf/ptypes/timestamp" "google.golang.org/api/option" "google.golang.org/api/support/bundler"