utils.go (212 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you 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 apm // import "go.elastic.co/apm/v2" import ( "context" "fmt" "math/rand" "os" "path/filepath" "reflect" "regexp" "runtime" "strings" "sync" "time" "github.com/pkg/errors" "go.elastic.co/apm/v2/internal/apmcloudutil" "go.elastic.co/apm/v2/internal/apmhostutil" "go.elastic.co/apm/v2/internal/apmlog" "go.elastic.co/apm/v2/internal/apmstrings" "go.elastic.co/apm/v2/model" ) var ( currentProcess model.Process goAgent = model.Agent{Name: "go", Version: AgentVersion} goLanguage = model.Language{Name: "go", Version: runtime.Version()} goRuntime = model.Runtime{Name: runtime.Compiler, Version: runtime.Version()} localSystem model.System cloudMetadataOnce sync.Once cloudMetadata *model.Cloud serviceNameInvalidRegexp = regexp.MustCompile("[^" + serviceNameValidClass + "]") labelKeyReplacer = strings.NewReplacer(`.`, `_`, `*`, `_`, `"`, `_`) rtypeBool = reflect.TypeOf(false) rtypeFloat64 = reflect.TypeOf(float64(0)) ) const ( envHostname = "ELASTIC_APM_HOSTNAME" envServiceNodeName = "ELASTIC_APM_SERVICE_NODE_NAME" serviceNameValidClass = "a-zA-Z0-9 _-" // At the time of writing, all keyword length limits // are 1024 runes, enforced by JSON Schema. stringLengthLimit = 1024 // Non-keyword string fields are not limited in length // by JSON Schema, but we still truncate all strings. // Some strings, such as database statement, we explicitly // allow to be longer than others. longStringLengthLimit = 10000 ) func init() { currentProcess = getCurrentProcess() localSystem = getLocalSystem() } func getCurrentProcess() model.Process { ppid := os.Getppid() title, err := currentProcessTitle() if err != nil || title == "" { title = filepath.Base(os.Args[0]) } return model.Process{ Pid: os.Getpid(), Ppid: &ppid, Title: truncateString(title), Argv: os.Args, } } func makeService(name, version, environment string) model.Service { service := model.Service{ Name: truncateString(name), Version: truncateString(version), Environment: truncateString(environment), Agent: &goAgent, Language: &goLanguage, Runtime: &goRuntime, } serviceNodeName := os.Getenv(envServiceNodeName) if serviceNodeName != "" { service.Node = &model.ServiceNode{ConfiguredName: truncateString(serviceNodeName)} } return service } func getLocalSystem() model.System { system := model.System{ Architecture: runtime.GOARCH, Platform: runtime.GOOS, } system.Hostname = os.Getenv(envHostname) if system.Hostname == "" { if hostname, err := os.Hostname(); err == nil { system.Hostname = hostname } } system.Hostname = truncateString(system.Hostname) if container, err := apmhostutil.Container(); err == nil { system.Container = container } system.Kubernetes = getKubernetesMetadata() return system } func getKubernetesMetadata() *model.Kubernetes { kubernetes, err := apmhostutil.Kubernetes() if err != nil { kubernetes = nil } namespace := os.Getenv("KUBERNETES_NAMESPACE") podName := os.Getenv("KUBERNETES_POD_NAME") podUID := os.Getenv("KUBERNETES_POD_UID") nodeName := os.Getenv("KUBERNETES_NODE_NAME") if namespace == "" && podName == "" && podUID == "" && nodeName == "" { return kubernetes } if kubernetes == nil { kubernetes = &model.Kubernetes{} } if namespace != "" { kubernetes.Namespace = namespace } if nodeName != "" { if kubernetes.Node == nil { kubernetes.Node = &model.KubernetesNode{} } kubernetes.Node.Name = nodeName } if podName != "" || podUID != "" { if kubernetes.Pod == nil { kubernetes.Pod = &model.KubernetesPod{} } if podName != "" { kubernetes.Pod.Name = podName } if podUID != "" { kubernetes.Pod.UID = podUID } } return kubernetes } func getCloudMetadata() *model.Cloud { // Querying cloud metadata can block, so we don't fetch it at // package initialisation time. Instead, we defer until it is // first requested by the tracer. cloudMetadataOnce.Do(func() { var logger apmcloudutil.Logger if l := apmlog.DefaultLogger(); l != nil { logger = l } provider := apmcloudutil.Auto if str := os.Getenv(envCloudProvider); str != "" { var err error provider, err = apmcloudutil.ParseProvider(str) if err != nil && logger != nil { logger.Warningf("disabling %q cloud metadata: %s", envCloudProvider, err) } } ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() var out model.Cloud if provider.GetCloudMetadata(ctx, logger, &out) { cloudMetadata = &out } }) return cloudMetadata } func cleanLabelKey(k string) string { return labelKeyReplacer.Replace(k) } // makeLabelValue returns v as a value suitable for including // in a label value. If v is numerical or boolean, then it will // be returned as-is; otherwise the value will be returned as a // string, using fmt.Sprint if necessary, and possibly truncated // using truncateString. func makeLabelValue(v interface{}) interface{} { switch v.(type) { case nil, bool, float32, float64, uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64: return v case string: return truncateString(v.(string)) } // Slow path. If v has a non-basic type whose underlying // type is convertible to bool or float64, return v as-is. // Otherwise, stringify. rtype := reflect.TypeOf(v) if rtype.ConvertibleTo(rtypeBool) || rtype.ConvertibleTo(rtypeFloat64) { // Custom type return v } return truncateString(fmt.Sprint(v)) } func validateServiceName(name string) error { idx := serviceNameInvalidRegexp.FindStringIndex(name) if idx == nil { return nil } return errors.Errorf( "invalid service name %q: character %q is not in the allowed set (%s)", name, name[idx[0]], serviceNameValidClass, ) } func sanitizeServiceName(name string) string { return serviceNameInvalidRegexp.ReplaceAllString(name, "_") } func truncateString(s string) string { s, _ = apmstrings.Truncate(s, stringLengthLimit) return s } func truncateLongString(s string) string { s, _ = apmstrings.Truncate(s, longStringLengthLimit) return s } func nextGracePeriod(p time.Duration) time.Duration { if p == -1 { return 0 } for i := time.Duration(0); i < 6; i++ { if p == (i * i * time.Second) { return (i + 1) * (i + 1) * time.Second } } return p } // jitterDuration returns d +/- some multiple of d in the range [0,j]. func jitterDuration(d time.Duration, rng *rand.Rand, j float64) time.Duration { if d == 0 || j == 0 { return d } r := (rng.Float64() * j * 2) - j return d + time.Duration(float64(d)*r) } func durationMicros(d time.Duration) float64 { us := d / time.Microsecond ns := d % time.Microsecond return float64(us) + float64(ns)/1e9 }