mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-27 04:15:55 +03:00
diagnostics: AppendUnique(), restructure sets, add metrics, fix bugs
This commit is contained in:
parent
703cf7bf8b
commit
6b3c2212a1
10 changed files with 141 additions and 82 deletions
3
caddy.go
3
caddy.go
|
@ -44,6 +44,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/diagnostics"
|
||||
)
|
||||
|
||||
// Configurable application parameters
|
||||
|
@ -573,6 +574,8 @@ func ValidateAndExecuteDirectives(cdyfile Input, inst *Instance, justValidate bo
|
|||
return err
|
||||
}
|
||||
|
||||
diagnostics.Set("num_server_blocks", len(sblocks))
|
||||
|
||||
return executeDirectives(inst, cdyfile.Path(), stype.Directives(), sblocks, justValidate)
|
||||
}
|
||||
|
||||
|
|
|
@ -152,18 +152,18 @@ func Run() {
|
|||
|
||||
// Begin diagnostics (these are no-ops if diagnostics disabled)
|
||||
diagnostics.Set("caddy_version", appVersion)
|
||||
// TODO: plugins
|
||||
diagnostics.Set("num_listeners", len(instance.Servers()))
|
||||
diagnostics.Set("server_type", serverType)
|
||||
diagnostics.Set("os", runtime.GOOS)
|
||||
diagnostics.Set("arch", runtime.GOARCH)
|
||||
diagnostics.Set("cpu", struct {
|
||||
NumLogical int `json:"num_logical"`
|
||||
AESNI bool `json:"aes_ni"`
|
||||
BrandName string `json:"brand_name"`
|
||||
BrandName string `json:"brand_name,omitempty"`
|
||||
NumLogical int `json:"num_logical,omitempty"`
|
||||
AESNI bool `json:"aes_ni,omitempty"`
|
||||
}{
|
||||
BrandName: cpuid.CPU.BrandName,
|
||||
NumLogical: runtime.NumCPU(),
|
||||
AESNI: cpuid.CPU.AesNi(),
|
||||
BrandName: cpuid.CPU.BrandName,
|
||||
})
|
||||
diagnostics.StartEmitting()
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/diagnostics"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
|
@ -369,6 +371,7 @@ func (p *parser) directive() error {
|
|||
|
||||
// The directive itself is appended as a relevant token
|
||||
p.block.Tokens[dir] = append(p.block.Tokens[dir], p.tokens[p.cursor])
|
||||
diagnostics.AppendUnique("directives", dir)
|
||||
|
||||
for p.Next() {
|
||||
if p.Val() == "{" {
|
||||
|
|
|
@ -24,6 +24,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/diagnostics"
|
||||
)
|
||||
|
||||
// tlsHandler is a http.Handler that will inject a value
|
||||
|
@ -97,6 +99,13 @@ func (h *tlsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
if checked {
|
||||
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 {
|
||||
|
|
|
@ -29,7 +29,6 @@ import (
|
|||
"github.com/mholt/caddy/caddyfile"
|
||||
"github.com/mholt/caddy/caddyhttp/staticfiles"
|
||||
"github.com/mholt/caddy/caddytls"
|
||||
"github.com/mholt/caddy/diagnostics"
|
||||
)
|
||||
|
||||
const serverType = "http"
|
||||
|
@ -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
|
||||
groups, err := groupSiteConfigsByListenAddr(h.siteConfigs)
|
||||
if err != nil {
|
||||
|
|
|
@ -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
|
||||
// so it can be referenced by middlewares
|
||||
|
|
|
@ -25,6 +25,8 @@ import (
|
|||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/diagnostics"
|
||||
)
|
||||
|
||||
// configGroup is a type that keys configs by their hostname
|
||||
|
@ -98,6 +100,23 @@ func (cg configGroup) GetConfigForClient(clientHello *tls.ClientHelloInfo) (*tls
|
|||
//
|
||||
// This method is safe for use as a tls.Config.GetCertificate callback.
|
||||
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)
|
||||
return &cert.Certificate, err
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ func Set(key string, val interface{}) {
|
|||
// 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,
|
||||
// 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?
|
||||
func Append(key string, value interface{}) {
|
||||
|
@ -142,66 +142,38 @@ func Append(key string, value interface{}) {
|
|||
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
|
||||
// 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 maps to a type that is not a string
|
||||
// set, an error is logged, and this is a no-op.
|
||||
func AppendUniqueString(key, value string) {
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
bufferMu.Lock()
|
||||
if bufferItemCount >= maxBufferItems {
|
||||
bufferMu.Unlock()
|
||||
return
|
||||
}
|
||||
bufVal, inBuffer := buffer[key]
|
||||
mapVal, mapOk := bufVal.(map[string]int)
|
||||
if inBuffer && !mapOk {
|
||||
setVal, setOk := bufVal.(countingSet)
|
||||
if inBuffer && !setOk {
|
||||
bufferMu.Unlock()
|
||||
log.Printf("[PANIC] Diagnostics: key %s already used for non-map value", key)
|
||||
log.Printf("[PANIC] Diagnostics: key %s already used for non-counting-set value", key)
|
||||
return
|
||||
}
|
||||
if mapVal == nil {
|
||||
buffer[key] = map[string]int{value: 1}
|
||||
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 mapOk {
|
||||
mapVal[value]++
|
||||
}
|
||||
bufferMu.Unlock()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
bufferMu.Lock()
|
||||
if bufferItemCount >= maxBufferItems {
|
||||
bufferMu.Unlock()
|
||||
return
|
||||
}
|
||||
bufVal, inBuffer := buffer[key]
|
||||
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++
|
||||
} else if mapOk {
|
||||
mapVal[value]++
|
||||
} else if setOk {
|
||||
// unique value already exists, so just increment counter
|
||||
setVal[value]++
|
||||
}
|
||||
bufferMu.Unlock()
|
||||
}
|
||||
|
@ -209,7 +181,7 @@ func AppendUniqueInt(key string, value int) {
|
|||
// Increment adds 1 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, an error is logged,
|
||||
// is not an integer, a panic is logged,
|
||||
// and this is a no-op.
|
||||
func Increment(key string) {
|
||||
incrementOrDecrement(key, true)
|
||||
|
|
|
@ -21,13 +21,16 @@
|
|||
// collection/aggregation functions. Call StartEmitting() when you are
|
||||
// ready to begin sending diagnostic updates.
|
||||
//
|
||||
// When collecting metrics (functions like Set, Append*, or Increment),
|
||||
// it may be desirable and even recommended to run invoke them in a new
|
||||
// When collecting metrics (functions like Set, AppendUnique, or Increment),
|
||||
// it may be desirable and even recommended to invoke them in a new
|
||||
// goroutine (use the go keyword) in case there is lock contention;
|
||||
// they are thread-safe (unless noted), and you may not want them to
|
||||
// block the main thread of execution. However, sometimes blocking
|
||||
// may be necessary too; for example, adding startup metrics to the
|
||||
// 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
|
||||
|
||||
import (
|
||||
|
@ -122,11 +125,6 @@ func emit(final bool) error {
|
|||
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,
|
||||
// just wait and try again -- this is a special case of
|
||||
// error that we handle differently, as you can see
|
||||
|
@ -151,6 +149,11 @@ func emit(final bool) error {
|
|||
// schedule the next update using our default update
|
||||
// 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
|
||||
// if the remote server didn't tell us to stop sending)
|
||||
if !final && !reply.Stop {
|
||||
|
@ -216,6 +219,30 @@ type Payload struct {
|
|||
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 (
|
||||
// httpClient should be used for HTTP requests. It
|
||||
// is configured with a timeout for reliability.
|
||||
|
@ -253,7 +280,7 @@ var (
|
|||
const (
|
||||
// endpoint is the base URL to remote diagnostics server;
|
||||
// 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
|
||||
// more diagnostic data. This value is only used if the
|
||||
|
|
57
plugins.go
57
plugins.go
|
@ -53,29 +53,59 @@ var (
|
|||
|
||||
// DescribePlugins returns a string describing the registered plugins.
|
||||
func DescribePlugins() string {
|
||||
pl := ListPlugins()
|
||||
|
||||
str := "Server types:\n"
|
||||
for name := range serverTypes {
|
||||
for _, name := range pl["server_types"] {
|
||||
str += " " + name + "\n"
|
||||
}
|
||||
|
||||
// List the loaders in registration order
|
||||
str += "\nCaddyfile loaders:\n"
|
||||
for _, loader := range caddyfileLoaders {
|
||||
str += " " + loader.name + "\n"
|
||||
}
|
||||
if defaultCaddyfileLoader.name != "" {
|
||||
str += " " + defaultCaddyfileLoader.name + "\n"
|
||||
for _, name := range pl["caddyfile_loaders"] {
|
||||
str += " " + name + "\n"
|
||||
}
|
||||
|
||||
if len(eventHooks) > 0 {
|
||||
// List the event hook plugins
|
||||
str += "\nEvent hook plugins:\n"
|
||||
for hookPlugin := range eventHooks {
|
||||
str += " hook." + hookPlugin + "\n"
|
||||
for _, name := range pl["event_hooks"] {
|
||||
str += " hook." + name + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Let's alphabetize the rest of these...
|
||||
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 {
|
||||
p["caddyfile_loaders"] = append(p["caddyfile_loaders"], loader.name)
|
||||
}
|
||||
if defaultCaddyfileLoader.name != "" {
|
||||
p["caddyfile_loaders"] = append(p["caddyfile_loaders"], defaultCaddyfileLoader.name)
|
||||
}
|
||||
|
||||
// event hook plugins
|
||||
if len(eventHooks) > 0 {
|
||||
for name := range eventHooks {
|
||||
p["event_hooks"] = append(p["event_hooks"], name)
|
||||
}
|
||||
}
|
||||
|
||||
// alphabetize the rest of the plugins
|
||||
var others []string
|
||||
for stype, stypePlugins := range plugins {
|
||||
for name := range stypePlugins {
|
||||
|
@ -89,12 +119,11 @@ func DescribePlugins() string {
|
|||
}
|
||||
|
||||
sort.Strings(others)
|
||||
str += "\nOther plugins:\n"
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue