mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
add per call stats field as histogram (#528)
* add per call stats field as histogram this will add a histogram of up to 240 data points of call data, produced every second, stored at the end of a call invocation in the db. the same metrics are also still shipped to prometheus (prometheus has the not-potentially-reduced version). for the API reference, see the updates to the swagger spec, this is just added onto the get call endpoint. this does not add any extra db calls and the field for stats in call is a json blob, which is easily modified to add / omit future fields. this is just tacked on to the call we're making to InsertCall, and expect this to add very little overhead; we are bounding the set to be relatively small, planning to clean out the db of calls periodically, functions will generally be short, and the same code used at a previous firm did not cause a notable db size increase with production workload that is worse, wrt histogram size (I checked). the code changes are really small aside from changing to strfmt.DateTime, adding a migration and implementing sql.Valuer; needed to slightly modify the swap function so that we can safely read `call.Stats` field to upload at end. with the full histogram in hand, we can compute max/min/average/median/growth rate/bernoulli distributions/whatever very easily in a UI or tooling. in particular, this data is easily chartable [for a UI], which is beneficial. * adds swagger spec of api update to calls endpoint * adds migration for call.stats field * adds call.stats field to sql queries * change swapping of hot logger to exec, so we know that call.Stats is no longer being modified after `exec` [in call.End] * throws out docker stats between function invocations in hot functions (no call to store them on, we could change this later for debug; they're in prom) * tested in tests and API closes #19 * add format of ints to swag
This commit is contained in:
@@ -3,10 +3,16 @@
|
||||
package drivers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
)
|
||||
|
||||
// A DriverCookie identifies a unique request to run a task.
|
||||
@@ -109,8 +115,50 @@ type ContainerTask interface {
|
||||
|
||||
// Stat is a bucket of stats from a driver at a point in time for a certain task.
|
||||
type Stat struct {
|
||||
Timestamp time.Time
|
||||
Metrics map[string]uint64
|
||||
Timestamp strfmt.DateTime `json:"timestamp"`
|
||||
Metrics map[string]uint64 `json:"metrics"`
|
||||
}
|
||||
|
||||
// Stats is a list of Stat, notably implements sql.Valuer
|
||||
type Stats []Stat
|
||||
|
||||
// implements sql.Valuer, returning a string
|
||||
func (s Stats) Value() (driver.Value, error) {
|
||||
if len(s) < 1 {
|
||||
return driver.Value(string("")), nil
|
||||
}
|
||||
var b bytes.Buffer
|
||||
err := json.NewEncoder(&b).Encode(s)
|
||||
// return a string type
|
||||
return driver.Value(b.String()), err
|
||||
}
|
||||
|
||||
// implements sql.Scanner
|
||||
func (s *Stats) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*s = nil
|
||||
return nil
|
||||
}
|
||||
bv, err := driver.String.ConvertValue(value)
|
||||
if err == nil {
|
||||
var b []byte
|
||||
switch x := bv.(type) {
|
||||
case []byte:
|
||||
b = x
|
||||
case string:
|
||||
b = []byte(x)
|
||||
}
|
||||
|
||||
if len(b) > 0 {
|
||||
return json.Unmarshal(b, s)
|
||||
}
|
||||
|
||||
*s = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// otherwise, return an error
|
||||
return fmt.Errorf("stats invalid db format: %T %T value, err: %v", value, bv, err)
|
||||
}
|
||||
|
||||
// TODO: ensure some type is applied to these statuses.
|
||||
@@ -149,15 +197,15 @@ func average(samples []Stat) (Stat, bool) {
|
||||
s := Stat{
|
||||
Metrics: samples[0].Metrics, // Recycle Metrics map from first sample
|
||||
}
|
||||
t := samples[0].Timestamp.UnixNano() / int64(l)
|
||||
t := time.Time(samples[0].Timestamp).UnixNano() / int64(l)
|
||||
for _, sample := range samples[1:] {
|
||||
t += sample.Timestamp.UnixNano() / int64(l)
|
||||
t += time.Time(sample.Timestamp).UnixNano() / int64(l)
|
||||
for k, v := range sample.Metrics {
|
||||
s.Metrics[k] += v
|
||||
}
|
||||
}
|
||||
|
||||
s.Timestamp = time.Unix(0, t)
|
||||
s.Timestamp = strfmt.DateTime(time.Unix(0, t))
|
||||
for k, v := range s.Metrics {
|
||||
s.Metrics[k] = v / uint64(l)
|
||||
}
|
||||
@@ -183,8 +231,8 @@ func Decimate(maxSamples int, stats []Stat) []Stat {
|
||||
return nil
|
||||
}
|
||||
|
||||
start := stats[0].Timestamp
|
||||
window := stats[len(stats)-1].Timestamp.Sub(start) / time.Duration(maxSamples)
|
||||
start := time.Time(stats[0].Timestamp)
|
||||
window := time.Time(stats[len(stats)-1].Timestamp).Sub(start) / time.Duration(maxSamples)
|
||||
|
||||
nextEntry, current := 0, start // nextEntry is the index tracking next Stats record location
|
||||
for x := 0; x < len(stats); {
|
||||
@@ -192,7 +240,7 @@ func Decimate(maxSamples int, stats []Stat) []Stat {
|
||||
|
||||
var samples []Stat
|
||||
for offset := 0; x+offset < len(stats); offset++ { // Iterate through samples until out of window
|
||||
if !isLastEntry && stats[x+offset].Timestamp.After(current.Add(window)) {
|
||||
if !isLastEntry && time.Time(stats[x+offset].Timestamp).After(current.Add(window)) {
|
||||
break
|
||||
}
|
||||
samples = stats[x : x+offset+1]
|
||||
|
||||
Reference in New Issue
Block a user