github.com/waldiirawan/apm-agent-go/v2@v2.2.2/utils.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package apm // import "github.com/waldiirawan/apm-agent-go/v2"
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"math/rand"
    24  	"os"
    25  	"path/filepath"
    26  	"reflect"
    27  	"regexp"
    28  	"runtime"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/pkg/errors"
    34  
    35  	"github.com/waldiirawan/apm-agent-go/v2/internal/apmcloudutil"
    36  	"github.com/waldiirawan/apm-agent-go/v2/internal/apmhostutil"
    37  	"github.com/waldiirawan/apm-agent-go/v2/internal/apmlog"
    38  	"github.com/waldiirawan/apm-agent-go/v2/internal/apmstrings"
    39  	"github.com/waldiirawan/apm-agent-go/v2/model"
    40  )
    41  
    42  var (
    43  	currentProcess model.Process
    44  	goAgent        = model.Agent{Name: "go", Version: AgentVersion}
    45  	goLanguage     = model.Language{Name: "go", Version: runtime.Version()}
    46  	goRuntime      = model.Runtime{Name: runtime.Compiler, Version: runtime.Version()}
    47  	localSystem    model.System
    48  
    49  	cloudMetadataOnce sync.Once
    50  	cloudMetadata     *model.Cloud
    51  
    52  	serviceNameInvalidRegexp = regexp.MustCompile("[^" + serviceNameValidClass + "]")
    53  	labelKeyReplacer         = strings.NewReplacer(`.`, `_`, `*`, `_`, `"`, `_`)
    54  
    55  	rtypeBool    = reflect.TypeOf(false)
    56  	rtypeFloat64 = reflect.TypeOf(float64(0))
    57  )
    58  
    59  const (
    60  	envHostname        = "ELASTIC_APM_HOSTNAME"
    61  	envServiceNodeName = "ELASTIC_APM_SERVICE_NODE_NAME"
    62  
    63  	serviceNameValidClass = "a-zA-Z0-9 _-"
    64  
    65  	// At the time of writing, all keyword length limits
    66  	// are 1024 runes, enforced by JSON Schema.
    67  	stringLengthLimit = 1024
    68  
    69  	// Non-keyword string fields are not limited in length
    70  	// by JSON Schema, but we still truncate all strings.
    71  	// Some strings, such as database statement, we explicitly
    72  	// allow to be longer than others.
    73  	longStringLengthLimit = 10000
    74  )
    75  
    76  func init() {
    77  	currentProcess = getCurrentProcess()
    78  	localSystem = getLocalSystem()
    79  }
    80  
    81  func getCurrentProcess() model.Process {
    82  	ppid := os.Getppid()
    83  	title, err := currentProcessTitle()
    84  	if err != nil || title == "" {
    85  		title = filepath.Base(os.Args[0])
    86  	}
    87  	return model.Process{
    88  		Pid:   os.Getpid(),
    89  		Ppid:  &ppid,
    90  		Title: truncateString(title),
    91  		Argv:  os.Args,
    92  	}
    93  }
    94  
    95  func makeService(name, version, environment string) model.Service {
    96  	service := model.Service{
    97  		Name:        truncateString(name),
    98  		Version:     truncateString(version),
    99  		Environment: truncateString(environment),
   100  		Agent:       &goAgent,
   101  		Language:    &goLanguage,
   102  		Runtime:     &goRuntime,
   103  	}
   104  
   105  	serviceNodeName := os.Getenv(envServiceNodeName)
   106  	if serviceNodeName != "" {
   107  		service.Node = &model.ServiceNode{ConfiguredName: truncateString(serviceNodeName)}
   108  	}
   109  
   110  	return service
   111  }
   112  
   113  func getLocalSystem() model.System {
   114  	system := model.System{
   115  		Architecture: runtime.GOARCH,
   116  		Platform:     runtime.GOOS,
   117  	}
   118  	system.Hostname = os.Getenv(envHostname)
   119  	if system.Hostname == "" {
   120  		if hostname, err := os.Hostname(); err == nil {
   121  			system.Hostname = hostname
   122  		}
   123  	}
   124  	system.Hostname = truncateString(system.Hostname)
   125  	if container, err := apmhostutil.Container(); err == nil {
   126  		system.Container = container
   127  	}
   128  	system.Kubernetes = getKubernetesMetadata()
   129  	return system
   130  }
   131  
   132  func getKubernetesMetadata() *model.Kubernetes {
   133  	kubernetes, err := apmhostutil.Kubernetes()
   134  	if err != nil {
   135  		kubernetes = nil
   136  	}
   137  	namespace := os.Getenv("KUBERNETES_NAMESPACE")
   138  	podName := os.Getenv("KUBERNETES_POD_NAME")
   139  	podUID := os.Getenv("KUBERNETES_POD_UID")
   140  	nodeName := os.Getenv("KUBERNETES_NODE_NAME")
   141  	if namespace == "" && podName == "" && podUID == "" && nodeName == "" {
   142  		return kubernetes
   143  	}
   144  	if kubernetes == nil {
   145  		kubernetes = &model.Kubernetes{}
   146  	}
   147  	if namespace != "" {
   148  		kubernetes.Namespace = namespace
   149  	}
   150  	if nodeName != "" {
   151  		if kubernetes.Node == nil {
   152  			kubernetes.Node = &model.KubernetesNode{}
   153  		}
   154  		kubernetes.Node.Name = nodeName
   155  	}
   156  	if podName != "" || podUID != "" {
   157  		if kubernetes.Pod == nil {
   158  			kubernetes.Pod = &model.KubernetesPod{}
   159  		}
   160  		if podName != "" {
   161  			kubernetes.Pod.Name = podName
   162  		}
   163  		if podUID != "" {
   164  			kubernetes.Pod.UID = podUID
   165  		}
   166  	}
   167  	return kubernetes
   168  }
   169  
   170  func getCloudMetadata() *model.Cloud {
   171  	// Querying cloud metadata can block, so we don't fetch it at
   172  	// package initialisation time. Instead, we defer until it is
   173  	// first requested by the tracer.
   174  	cloudMetadataOnce.Do(func() {
   175  		var logger apmcloudutil.Logger
   176  		if l := apmlog.DefaultLogger(); l != nil {
   177  			logger = l
   178  		}
   179  		provider := apmcloudutil.Auto
   180  		if str := os.Getenv(envCloudProvider); str != "" {
   181  			var err error
   182  			provider, err = apmcloudutil.ParseProvider(str)
   183  			if err != nil && logger != nil {
   184  				logger.Warningf("disabling %q cloud metadata: %s", envCloudProvider, err)
   185  			}
   186  		}
   187  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   188  		defer cancel()
   189  		var out model.Cloud
   190  		if provider.GetCloudMetadata(ctx, logger, &out) {
   191  			cloudMetadata = &out
   192  		}
   193  	})
   194  	return cloudMetadata
   195  }
   196  
   197  func cleanLabelKey(k string) string {
   198  	return labelKeyReplacer.Replace(k)
   199  }
   200  
   201  // makeLabelValue returns v as a value suitable for including
   202  // in a label value. If v is numerical or boolean, then it will
   203  // be returned as-is; otherwise the value will be returned as a
   204  // string, using fmt.Sprint if necessary, and possibly truncated
   205  // using truncateString.
   206  func makeLabelValue(v interface{}) interface{} {
   207  	switch v.(type) {
   208  	case nil, bool, float32, float64,
   209  		uint, uint8, uint16, uint32, uint64,
   210  		int, int8, int16, int32, int64:
   211  		return v
   212  	case string:
   213  		return truncateString(v.(string))
   214  	}
   215  	// Slow path. If v has a non-basic type whose underlying
   216  	// type is convertible to bool or float64, return v as-is.
   217  	// Otherwise, stringify.
   218  	rtype := reflect.TypeOf(v)
   219  	if rtype.ConvertibleTo(rtypeBool) || rtype.ConvertibleTo(rtypeFloat64) {
   220  		// Custom type
   221  		return v
   222  	}
   223  	return truncateString(fmt.Sprint(v))
   224  }
   225  
   226  func validateServiceName(name string) error {
   227  	idx := serviceNameInvalidRegexp.FindStringIndex(name)
   228  	if idx == nil {
   229  		return nil
   230  	}
   231  	return errors.Errorf(
   232  		"invalid service name %q: character %q is not in the allowed set (%s)",
   233  		name, name[idx[0]], serviceNameValidClass,
   234  	)
   235  }
   236  
   237  func sanitizeServiceName(name string) string {
   238  	return serviceNameInvalidRegexp.ReplaceAllString(name, "_")
   239  }
   240  
   241  func truncateString(s string) string {
   242  	s, _ = apmstrings.Truncate(s, stringLengthLimit)
   243  	return s
   244  }
   245  
   246  func truncateLongString(s string) string {
   247  	s, _ = apmstrings.Truncate(s, longStringLengthLimit)
   248  	return s
   249  }
   250  
   251  func nextGracePeriod(p time.Duration) time.Duration {
   252  	if p == -1 {
   253  		return 0
   254  	}
   255  	for i := time.Duration(0); i < 6; i++ {
   256  		if p == (i * i * time.Second) {
   257  			return (i + 1) * (i + 1) * time.Second
   258  		}
   259  	}
   260  	return p
   261  }
   262  
   263  // jitterDuration returns d +/- some multiple of d in the range [0,j].
   264  func jitterDuration(d time.Duration, rng *rand.Rand, j float64) time.Duration {
   265  	if d == 0 || j == 0 {
   266  		return d
   267  	}
   268  	r := (rng.Float64() * j * 2) - j
   269  	return d + time.Duration(float64(d)*r)
   270  }
   271  
   272  func durationMicros(d time.Duration) float64 {
   273  	us := d / time.Microsecond
   274  	ns := d % time.Microsecond
   275  	return float64(us) + float64(ns)/1e9
   276  }