caddy/telemetry/collection.go
Matthew Holt 9160789b42
telemetry: Make http_user_agent a normalized field
This way we store a short 8-byte hash of the UA instead of the full
string; exactly the same way we store TLS ClientHello info.
2018-05-10 08:57:25 -06:00

308 lines
7.9 KiB
Go

// Copyright 2015 Light Code Labs, 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 telemetry
import (
"fmt"
"hash/fnv"
"log"
"strings"
"github.com/google/uuid"
)
// Init initializes this package so that it may
// be used. Do not call this function more than
// once. Init panics if it is called more than
// once or if the UUID value is empty. Once this
// function is called, the rest of the package
// may safely be used. If this function is not
// called, the collector functions may still be
// invoked, but they will be no-ops.
//
// Any metrics keys that are passed in the second
// argument will be permanently disabled for the
// lifetime of the process.
func Init(instanceID uuid.UUID, disabledMetricsKeys []string) {
if enabled {
panic("already initialized")
}
if str := instanceID.String(); str == "" ||
str == "00000000-0000-0000-0000-000000000000" {
panic("empty UUID")
}
instanceUUID = instanceID
disabledMetricsMu.Lock()
for _, key := range disabledMetricsKeys {
disabledMetrics[strings.TrimSpace(key)] = false
}
disabledMetricsMu.Unlock()
enabled = true
}
// StartEmitting sends the current payload and begins the
// transmission cycle for updates. This is the first
// update sent, and future ones will be sent until
// StopEmitting is called.
//
// This function is non-blocking (it spawns a new goroutine).
//
// This function panics if it was called more than once.
// It is a no-op if this package was not initialized.
func StartEmitting() {
if !enabled {
return
}
updateTimerMu.Lock()
if updateTimer != nil {
updateTimerMu.Unlock()
panic("updates already started")
}
updateTimerMu.Unlock()
updateMu.Lock()
if updating {
updateMu.Unlock()
panic("update already in progress")
}
updateMu.Unlock()
go logEmit(false)
}
// StopEmitting sends the current payload and terminates
// the update cycle. No more updates will be sent.
//
// It is a no-op if the package was never initialized
// or if emitting was never started.
//
// NOTE: This function is blocking. Run in a goroutine if
// you want to guarantee no blocking at critical times
// like exiting the program.
func StopEmitting() {
if !enabled {
return
}
updateTimerMu.Lock()
if updateTimer == nil {
updateTimerMu.Unlock()
return
}
updateTimerMu.Unlock()
logEmit(true) // likely too early; may take minutes to return
}
// Reset empties the current payload buffer.
func Reset() {
resetBuffer()
}
// Set puts a value in the buffer to be included
// in the next emission. It overwrites any
// previous value.
//
// This function is safe for multiple goroutines,
// and it is recommended to call this using the
// go keyword after the call to SendHello so it
// doesn't block crucial code.
func Set(key string, val interface{}) {
if !enabled || isDisabled(key) {
return
}
bufferMu.Lock()
if _, ok := buffer[key]; !ok {
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
bufferItemCount++
}
buffer[key] = val
bufferMu.Unlock()
}
// SetNested puts a value in the buffer to be included
// in the next emission, nested under the top-level key
// as subkey. It overwrites any previous value.
//
// This function is safe for multiple goroutines,
// and it is recommended to call this using the
// go keyword after the call to SendHello so it
// doesn't block crucial code.
func SetNested(key, subkey string, val interface{}) {
if !enabled || isDisabled(key) {
return
}
bufferMu.Lock()
if topLevel, ok1 := buffer[key]; ok1 {
topLevelMap, ok2 := topLevel.(map[string]interface{})
if !ok2 {
bufferMu.Unlock()
log.Printf("[PANIC] Telemetry: key %s is already used for non-nested-map value", key)
return
}
if _, ok3 := topLevelMap[subkey]; !ok3 {
// don't exceed max buffer size
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
bufferItemCount++
}
topLevelMap[subkey] = val
} else {
// don't exceed max buffer size
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
bufferItemCount++
buffer[key] = map[string]interface{}{subkey: val}
}
bufferMu.Unlock()
}
// Append appends value to a list named key.
// If key is new, a new list will be created.
// If key maps to a type that is not a list,
// a panic is logged, and this is a no-op.
func Append(key string, value interface{}) {
if !enabled || isDisabled(key) {
return
}
bufferMu.Lock()
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
// TODO: Test this...
bufVal, inBuffer := buffer[key]
sliceVal, sliceOk := bufVal.([]interface{})
if inBuffer && !sliceOk {
bufferMu.Unlock()
log.Printf("[PANIC] Telemetry: key %s already used for non-slice value", key)
return
}
if sliceVal == nil {
buffer[key] = []interface{}{value}
} else if sliceOk {
buffer[key] = append(sliceVal, value)
}
bufferItemCount++
bufferMu.Unlock()
}
// AppendUnique adds value to a set named key.
// Set items are unordered. Values in the set
// are unique, but how many times they are
// appended is counted. The value must be
// hashable.
//
// If key is new, a new set will be created for
// values with that key. If key maps to a type
// that is not a counting set, a panic is logged,
// and this is a no-op.
func AppendUnique(key string, value interface{}) {
if !enabled || isDisabled(key) {
return
}
bufferMu.Lock()
bufVal, inBuffer := buffer[key]
setVal, setOk := bufVal.(countingSet)
if inBuffer && !setOk {
bufferMu.Unlock()
log.Printf("[PANIC] Telemetry: key %s already used for non-counting-set value", key)
return
}
if setVal == nil {
// ensure the buffer is not too full, then add new unique value
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
buffer[key] = countingSet{value: 1}
bufferItemCount++
} else if setOk {
// unique value already exists, so just increment counter
setVal[value]++
}
bufferMu.Unlock()
}
// Add adds amount to a value named key.
// If it does not exist, it is created with
// a value of 1. If key maps to a type that
// is not an integer, a panic is logged,
// and this is a no-op.
func Add(key string, amount int) {
atomicAdd(key, amount)
}
// Increment is a shortcut for Add(key, 1)
func Increment(key string) {
atomicAdd(key, 1)
}
// atomicAdd adds amount (negative to subtract)
// to key.
func atomicAdd(key string, amount int) {
if !enabled || isDisabled(key) {
return
}
bufferMu.Lock()
bufVal, inBuffer := buffer[key]
intVal, intOk := bufVal.(int)
if inBuffer && !intOk {
bufferMu.Unlock()
log.Printf("[PANIC] Telemetry: key %s already used for non-integer value", key)
return
}
if !inBuffer {
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
bufferItemCount++
}
buffer[key] = intVal + amount
bufferMu.Unlock()
}
// FastHash hashes input using a 32-bit hashing algorithm
// that is fast, and returns the hash as a hex-encoded string.
// Do not use this for cryptographic purposes.
func FastHash(input []byte) string {
h := fnv.New32a()
h.Write(input)
return fmt.Sprintf("%x", h.Sum32())
}
// isDisabled returns whether key is
// a disabled metric key. ALL collection
// functions should call this and not
// save the value if this returns true.
func isDisabled(key string) bool {
// for keys that are augmented with data, such as
// "tls_client_hello_ua:<hash>", just
// check the prefix "tls_client_hello_ua"
checkKey := key
if idx := strings.Index(key, ":"); idx > -1 {
checkKey = key[:idx]
}
disabledMetricsMu.RLock()
_, ok := disabledMetrics[checkKey]
disabledMetricsMu.RUnlock()
return ok
}