Commit 6b3c2212 authored by Matthew Holt's avatar Matthew Holt

diagnostics: AppendUnique(), restructure sets, add metrics, fix bugs

parent 703cf7bf
...@@ -44,6 +44,7 @@ import ( ...@@ -44,6 +44,7 @@ import (
"time" "time"
"github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/diagnostics"
) )
// Configurable application parameters // Configurable application parameters
...@@ -573,6 +574,8 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo ...@@ -573,6 +574,8 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
return err return err
} }
diagnostics.Set("num_server_blocks", len(sblocks))
return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate) return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
} }
......
...@@ -152,18 +152,18 @@ func Run() { ...@@ -152,18 +152,18 @@ func Run() {
// Begin diagnostics (these are no-ops if diagnostics disabled) // Begin diagnostics (these are no-ops if diagnostics disabled)
diagnostics.Set("caddy_version", appVersion) diagnostics.Set("caddy_version", appVersion)
// TODO: plugins
diagnostics.Set("num_listeners", len(instance.Servers())) diagnostics.Set("num_listeners", len(instance.Servers()))
diagnostics.Set("server_type", serverType)
diagnostics.Set("os", runtime.GOOS) diagnostics.Set("os", runtime.GOOS)
diagnostics.Set("arch", runtime.GOARCH) diagnostics.Set("arch", runtime.GOARCH)
diagnostics.Set("cpu", struct { diagnostics.Set("cpu", struct {
NumLogical int `json:"num_logical"` BrandName string `json:"brand_name,omitempty"`
AESNI bool `json:"aes_ni"` NumLogical int `json:"num_logical,omitempty"`
BrandName string `json:"brand_name"` AESNI bool `json:"aes_ni,omitempty"`
}{ }{
BrandName: cpuid.CPU.BrandName,
NumLogical: runtime.NumCPU(), NumLogical: runtime.NumCPU(),
AESNI: cpuid.CPU.AesNi(), AESNI: cpuid.CPU.AesNi(),
BrandName: cpuid.CPU.BrandName,
}) })
diagnostics.StartEmitting() diagnostics.StartEmitting()
......
...@@ -20,6 +20,8 @@ import ( ...@@ -20,6 +20,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/mholt/caddy/diagnostics"
) )
// Parse parses the input just enough to group tokens, in // Parse parses the input just enough to group tokens, in
...@@ -369,6 +371,7 @@ func (p *parser) directive() error { ...@@ -369,6 +371,7 @@ func (p *parser) directive() error {
// The directive itself is appended as a relevant token // The directive itself is appended as a relevant token
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor]) p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
diagnostics.AppendUnique("directives", dir)
for p.Next() { for p.Next() {
if p.Val() == "{" { if p.Val() == "{" {
......
...@@ -24,6 +24,8 @@ import ( ...@@ -24,6 +24,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"github.com/mholt/caddy/diagnostics"
) )
// tlsHandler is a http.Handler that will inject a value // tlsHandler is a http.Handler that will inject a value
...@@ -97,6 +99,13 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -97,6 +99,13 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if checked { if checked {
r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm)) r = r.WithContext(context.WithValue(r.Context(), MitmCtxKey, mitm))
if mitm {
go diagnostics.AppendUnique("mitm", "likely")
} else {
go diagnostics.AppendUnique("mitm", "unlikely")
}
} else {
go diagnostics.AppendUnique("mitm", "unknown")
} }
if mitm && h.closeOnMITM { if mitm && h.closeOnMITM {
......
...@@ -29,7 +29,6 @@ import ( ...@@ -29,7 +29,6 @@ import (
"github.com/mholt/caddy/caddyfile" "github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/caddyhttp/staticfiles" "github.com/mholt/caddy/caddyhttp/staticfiles"
"github.com/mholt/caddy/caddytls" "github.com/mholt/caddy/caddytls"
"github.com/mholt/caddy/diagnostics"
) )
const serverType = "http" const serverType = "http"
...@@ -206,8 +205,6 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) { ...@@ -206,8 +205,6 @@ func (h *httpContext) MakeServers() ([]caddy.Server, error) {
} }
} }
diagnostics.Set("num_sites", len(h.siteConfigs))
// we must map (group) each config to a bind address // we must map (group) each config to a bind address
groups, err := groupSiteConfigsByListenAddr(h.siteConfigs) groups, err := groupSiteConfigsByListenAddr(h.siteConfigs)
if err != nil { if err != nil {
......
...@@ -346,7 +346,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -346,7 +346,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
}() }()
go diagnostics.AppendUniqueString("user_agent", r.Header.Get("User-Agent")) go diagnostics.AppendUnique("user_agent", r.Header.Get("User-Agent"))
// copy the original, unchanged URL into the context // copy the original, unchanged URL into the context
// so it can be referenced by middlewares // so it can be referenced by middlewares
......
...@@ -25,6 +25,8 @@ import ( ...@@ -25,6 +25,8 @@ import (
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/mholt/caddy/diagnostics"
) )
// configGroup is a type that keys configs by their hostname // configGroup is a type that keys configs by their hostname
...@@ -98,6 +100,23 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls ...@@ -98,6 +100,23 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls
// //
// This method is safe for use as a tls.Config.GetCertificate callback. // This method is safe for use as a tls.Config.GetCertificate callback.
func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
go diagnostics.Append("client_hello", struct {
NoSNI bool `json:"no_sni,omitempty"`
CipherSuites []uint16 `json:"cipher_suites,omitempty"`
SupportedCurves []tls.CurveID `json:"curves,omitempty"`
SupportedPoints []uint8 `json:"points,omitempty"`
SignatureSchemes []tls.SignatureScheme `json:"sig_scheme,omitempty"`
ALPN []string `json:"alpn,omitempty"`
SupportedVersions []uint16 `json:"versions,omitempty"`
}{
NoSNI: clientHello.ServerName == "",
CipherSuites: clientHello.CipherSuites,
SupportedCurves: clientHello.SupportedCurves,
SupportedPoints: clientHello.SupportedPoints,
SignatureSchemes: clientHello.SignatureSchemes,
ALPN: clientHello.SupportedProtos,
SupportedVersions: clientHello.SupportedVersions,
})
cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true) cert, err := cfg.getCertDuringHandshake(strings.ToLower(clientHello.ServerName), true, true)
return &cert.Certificate, err return &cert.Certificate, err
} }
......
...@@ -113,7 +113,7 @@ func Set(key string, val interface{}) { ...@@ -113,7 +113,7 @@ func Set(key string, val interface{}) {
// Append appends value to a list named key. // Append appends value to a list named key.
// If key is new, a new list will be created. // If key is new, a new list will be created.
// If key maps to a type that is not a list, // If key maps to a type that is not a list,
// an error is logged, and this is a no-op. // a panic is logged, and this is a no-op.
// //
// TODO: is this function needed/useful? // TODO: is this function needed/useful?
func Append(key string, value interface{}) { func Append(key string, value interface{}) {
...@@ -142,66 +142,38 @@ func Append(key string, value interface{}) { ...@@ -142,66 +142,38 @@ func Append(key string, value interface{}) {
bufferMu.Unlock() bufferMu.Unlock()
} }
// AppendUniqueString adds value to a set named key. // AppendUnique adds value to a set namedkey.
// Set items are unordered. Values in the set // Set items are unordered. Values in the set
// are unique, but repeat values are counted. // are unique, but how many times they are
// appended is counted.
// //
// If key is new, a new set will be created. // If key is new, a new set will be created for
// If key maps to a type that is not a string // values with that key. If key maps to a type
// set, an error is logged, and this is a no-op. // that is not a counting set, a panic is logged,
func AppendUniqueString(key, value string) { // and this is a no-op.
func AppendUnique(key string, value interface{}) {
if !enabled { if !enabled {
return return
} }
bufferMu.Lock() bufferMu.Lock()
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
bufVal, inBuffer := buffer[key] bufVal, inBuffer := buffer[key]
mapVal, mapOk := bufVal.(map[string]int) setVal, setOk := bufVal.(countingSet)
if inBuffer && !mapOk { if inBuffer && !setOk {
bufferMu.Unlock()
log.Printf("[PANIC] Diagnostics: key %s already used for non-map value", key)
return
}
if mapVal == nil {
buffer[key] = map[string]int{value: 1}
bufferItemCount++
} else if mapOk {
mapVal[value]++
}
bufferMu.Unlock() bufferMu.Unlock()
} log.Printf("[PANIC] Diagnostics: key %s already used for non-counting-set value", key)
// AppendUniqueInt adds value to a set named key.
// Set items are unordered. Values in the set
// are unique, but repeat values are counted.
//
// If key is new, a new set will be created.
// If key maps to a type that is not an integer
// set, an error is logged, and this is a no-op.
func AppendUniqueInt(key string, value int) {
if !enabled {
return return
} }
bufferMu.Lock() if setVal == nil {
// ensure the buffer is not too full, then add new unique value
if bufferItemCount >= maxBufferItems { if bufferItemCount >= maxBufferItems {
bufferMu.Unlock() bufferMu.Unlock()
return return
} }
bufVal, inBuffer := buffer[key] buffer[key] = countingSet{value: 1}
mapVal, mapOk := bufVal.(map[int]int)
if inBuffer && !mapOk {
bufferMu.Unlock()
log.Printf("[PANIC] Diagnostics: key %s already used for non-map value", key)
return
}
if mapVal == nil {
buffer[key] = map[int]int{value: 1}
bufferItemCount++ bufferItemCount++
} else if mapOk { } else if setOk {
mapVal[value]++ // unique value already exists, so just increment counter
setVal[value]++
} }
bufferMu.Unlock() bufferMu.Unlock()
} }
...@@ -209,7 +181,7 @@ func AppendUniqueInt(key string, value int) { ...@@ -209,7 +181,7 @@ func AppendUniqueInt(key string, value int) {
// Increment adds 1 to a value named key. // Increment adds 1 to a value named key.
// If it does not exist, it is created with // If it does not exist, it is created with
// a value of 1. If key maps to a type that // a value of 1. If key maps to a type that
// is not an integer, an error is logged, // is not an integer, a panic is logged,
// and this is a no-op. // and this is a no-op.
func Increment(key string) { func Increment(key string) {
incrementOrDecrement(key, true) incrementOrDecrement(key, true)
......
...@@ -21,13 +21,16 @@ ...@@ -21,13 +21,16 @@
// collection/aggregation functions. Call StartEmitting() when you are // collection/aggregation functions. Call StartEmitting() when you are
// ready to begin sending diagnostic updates. // ready to begin sending diagnostic updates.
// //
// When collecting metrics (functions like Set, Append*, or Increment), // When collecting metrics (functions like Set, AppendUnique, or Increment),
// it may be desirable and even recommended to run invoke them in a new // it may be desirable and even recommended to invoke them in a new
// goroutine (use the go keyword) in case there is lock contention; // goroutine (use the go keyword) in case there is lock contention;
// they are thread-safe (unless noted), and you may not want them to // they are thread-safe (unless noted), and you may not want them to
// block the main thread of execution. However, sometimes blocking // block the main thread of execution. However, sometimes blocking
// may be necessary too; for example, adding startup metrics to the // may be necessary too; for example, adding startup metrics to the
// buffer before the call to StartEmitting(). // buffer before the call to StartEmitting().
//
// This package is designed to be as fast and space-efficient as reasonably
// possible, so that it does not disrupt the flow of execution.
package diagnostics package diagnostics
import ( import (
...@@ -122,11 +125,6 @@ func emit(final bool) error { ...@@ -122,11 +125,6 @@ func emit(final bool) error {
continue continue
} }
// ensure we won't slam the diagnostics server
if reply.NextUpdate < 1*time.Second {
reply.NextUpdate = defaultUpdateInterval
}
// make sure we didn't send the update too soon; if so, // make sure we didn't send the update too soon; if so,
// just wait and try again -- this is a special case of // just wait and try again -- this is a special case of
// error that we handle differently, as you can see // error that we handle differently, as you can see
...@@ -151,6 +149,11 @@ func emit(final bool) error { ...@@ -151,6 +149,11 @@ func emit(final bool) error {
// schedule the next update using our default update // schedule the next update using our default update
// interval because the server might be healthy later // interval because the server might be healthy later
// ensure we won't slam the diagnostics server
if reply.NextUpdate < 1*time.Second {
reply.NextUpdate = defaultUpdateInterval
}
// schedule the next update (if this wasn't the last one and // schedule the next update (if this wasn't the last one and
// if the remote server didn't tell us to stop sending) // if the remote server didn't tell us to stop sending)
if !final && !reply.Stop { if !final && !reply.Stop {
...@@ -216,6 +219,30 @@ type Payload struct { ...@@ -216,6 +219,30 @@ type Payload struct {
Data map[string]interface{} `json:"data,omitempty"` Data map[string]interface{} `json:"data,omitempty"`
} }
// countingSet implements a set that counts how many
// times a key is inserted. It marshals to JSON in a
// way such that keys are converted to values next
// to their associated counts.
type countingSet map[interface{}]int
// MarshalJSON implements the json.Marshaler interface.
// It converts the set to an array so that the values
// are JSON object values instead of keys, since keys
// are difficult to query in databases.
func (s countingSet) MarshalJSON() ([]byte, error) {
type Item struct {
Value interface{} `json:"value"`
Count int `json:"count"`
}
var list []Item
for k, v := range s {
list = append(list, Item{Value: k, Count: v})
}
return json.Marshal(list)
}
var ( var (
// httpClient should be used for HTTP requests. It // httpClient should be used for HTTP requests. It
// is configured with a timeout for reliability. // is configured with a timeout for reliability.
...@@ -253,7 +280,7 @@ var ( ...@@ -253,7 +280,7 @@ var (
const ( const (
// endpoint is the base URL to remote diagnostics server; // endpoint is the base URL to remote diagnostics server;
// the instance ID will be appended to it. // the instance ID will be appended to it.
endpoint = "https://diagnostics-staging.caddyserver.com/update/" // TODO: make configurable, "http://localhost:8081/update/" endpoint = "https://diagnostics-staging.caddyserver.com/update/" // TODO: make configurable, "http://localhost:8085/update/"
// defaultUpdateInterval is how long to wait before emitting // defaultUpdateInterval is how long to wait before emitting
// more diagnostic data. This value is only used if the // more diagnostic data. This value is only used if the
......
...@@ -53,29 +53,59 @@ var ( ...@@ -53,29 +53,59 @@ var (
// DescribePlugins returns a string describing the registered plugins. // DescribePlugins returns a string describing the registered plugins.
func DescribePlugins() string { func DescribePlugins() string {
pl := ListPlugins()
str := "Server types:\n" str := "Server types:\n"
for name := range serverTypes { for _, name := range pl["server_types"] {
str += " " + name + "\n" str += " " + name + "\n"
} }
// List the loaders in registration order
str += "\nCaddyfile loaders:\n" str += "\nCaddyfile loaders:\n"
for _, name := range pl["caddyfile_loaders"] {
str += " " + name + "\n"
}
if len(eventHooks) > 0 {
str += "\nEvent hook plugins:\n"
for _, name := range pl["event_hooks"] {
str += " hook." + name + "\n"
}
}
str += "\nOther plugins:\n"
for _, name := range pl["others"] {
str += " " + name + "\n"
}
return str
}
// ListPlugins makes a list of the registered plugins,
// keyed by plugin type.
func ListPlugins() map[string][]string {
p := make(map[string][]string)
// server type plugins
for name := range serverTypes {
p["server_types"] = append(p["server_types"], name)
}
// caddyfile loaders in registration order
for _, loader := range caddyfileLoaders { for _, loader := range caddyfileLoaders {
str += " " + loader.name + "\n" p["caddyfile_loaders"] = append(p["caddyfile_loaders"], loader.name)
} }
if defaultCaddyfileLoader.name != "" { if defaultCaddyfileLoader.name != "" {
str += " " + defaultCaddyfileLoader.name + "\n" p["caddyfile_loaders"] = append(p["caddyfile_loaders"], defaultCaddyfileLoader.name)
} }
// event hook plugins
if len(eventHooks) > 0 { if len(eventHooks) > 0 {
// List the event hook plugins for name := range eventHooks {
str += "\nEvent hook plugins:\n" p["event_hooks"] = append(p["event_hooks"], name)
for hookPlugin := range eventHooks {
str += " hook." + hookPlugin + "\n"
} }
} }
// Let's alphabetize the rest of these... // alphabetize the rest of the plugins
var others []string var others []string
for stype, stypePlugins := range plugins { for stype, stypePlugins := range plugins {
for name := range stypePlugins { for name := range stypePlugins {
...@@ -89,12 +119,11 @@ func DescribePlugins() string { ...@@ -89,12 +119,11 @@ func DescribePlugins() string {
} }
sort.Strings(others) sort.Strings(others)
str += "\nOther plugins:\n"
for _, name := range others { for _, name := range others {
str += " " + name + "\n" p["others"] = append(p["others"], name)
} }
return str return p
} }
// ValidDirectives returns the list of all directives that are // ValidDirectives returns the list of all directives that are
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment