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 }