mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -04:00 
			
		
		
		
	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.
		
			
				
	
	
		
			308 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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
 | |
| }
 |