Skip to content

Commit

Permalink
diagnostics: AppendUnique(), restructure sets, add metrics, fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
mholt committed Feb 10, 2018
1 parent 703cf7b commit 6b3c221
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 80 deletions.
3 changes: 3 additions & 0 deletions caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"time"

"github.com/mholt/caddy/caddyfile"
"github.com/mholt/caddy/diagnostics"
)

// Configurable application parameters
Expand Down Expand Up @@ -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)
}

Expand Down
10 changes: 5 additions & 5 deletions caddy/caddymain/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
3 changes: 3 additions & 0 deletions caddyfile/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"os"
"path/filepath"
"strings"

"github.com/mholt/caddy/diagnostics"
)

// Parse parses the input just enough to group tokens, in
Expand Down Expand Up @@ -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() == "{" {
Expand Down
9 changes: 9 additions & 0 deletions caddyhttp/httpserver/mitm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"strconv"
"strings"
"sync"

"github.com/mholt/caddy/diagnostics"
)

// tlsHandler is a http.Handler that will inject a value
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 0 additions & 3 deletions caddyhttp/httpserver/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion caddyhttp/httpserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions caddytls/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"sync"
"sync/atomic"
"time"

"github.com/mholt/caddy/diagnostics"
)

// configGroup is a type that keys configs by their hostname
Expand Down Expand Up @@ -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
}
Expand Down
74 changes: 23 additions & 51 deletions diagnostics/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}) {
Expand Down Expand Up @@ -142,74 +142,46 @@ 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 !enabled {
return
}
bufferMu.Lock()
if bufferItemCount >= maxBufferItems {
bufferMu.Unlock()
return
}
bufVal, inBuffer := buffer[key]
mapVal, mapOk := bufVal.(map[string]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[string]int{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 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[int]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[int]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]++
} else if setOk {
// unique value already exists, so just increment counter
setVal[value]++
}
bufferMu.Unlock()
}

// 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)
Expand Down
43 changes: 35 additions & 8 deletions diagnostics/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 6b3c221

Please sign in to comment.