Initial copy in
This commit is contained in:
@@ -36,11 +36,3 @@ linters:
|
|||||||
- thelper
|
- thelper
|
||||||
- varcheck
|
- varcheck
|
||||||
- varnamelen
|
- varnamelen
|
||||||
linters-settings:
|
|
||||||
tagliatelle:
|
|
||||||
case:
|
|
||||||
use-field-name: true
|
|
||||||
rules:
|
|
||||||
json: goCamel
|
|
||||||
wsl:
|
|
||||||
allow-separated-leading-comment: true
|
|
||||||
|
|||||||
147
client.go
Normal file
147
client.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
targets []*Target
|
||||||
|
hooks []Hook
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hook func(context.Context, *Event)
|
||||||
|
|
||||||
|
func New() *Client {
|
||||||
|
return &Client{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AddTarget(url string, headers map[string]string, writePeriodSeconds float64) *Target {
|
||||||
|
target := &Target{
|
||||||
|
client: resty.New().SetBaseURL(url).SetHeaders(headers),
|
||||||
|
writePeriodSeconds: writePeriodSeconds,
|
||||||
|
windowSeconds: 100.0,
|
||||||
|
stop: make(chan bool),
|
||||||
|
lastEvent: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
go c.flushLoop(target)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.targets = append(c.targets, target)
|
||||||
|
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AddHook(hook Hook) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.hooks = append(c.hooks, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Log(ctx context.Context, vals ...any) {
|
||||||
|
ev := newEvent("log", vals...)
|
||||||
|
c.writeEvent(ctx, ev)
|
||||||
|
|
||||||
|
parts := []string{}
|
||||||
|
|
||||||
|
for i := 0; i < len(vals); i += 2 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s=%s", vals[i], vals[i+1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Print(strings.Join(parts, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, target := range c.targets {
|
||||||
|
close(target.stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) writeEvent(ctx context.Context, ev *Event) {
|
||||||
|
ev.Set("durationMS", time.Since(ev.start).Milliseconds())
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
for _, hook := range c.hooks {
|
||||||
|
hook(ctx, ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, target := range c.targets {
|
||||||
|
target.writeEvent(ev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) flushLoop(target *Target) {
|
||||||
|
t := time.NewTicker(time.Duration(target.writePeriodSeconds * float64(time.Second)))
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-target.stop:
|
||||||
|
c.flush(target)
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-t.C:
|
||||||
|
c.flush(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) flush(target *Target) {
|
||||||
|
c.mu.Lock()
|
||||||
|
events := target.events
|
||||||
|
target.events = nil
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if len(events) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
g := gzip.NewWriter(buf)
|
||||||
|
enc := json.NewEncoder(g)
|
||||||
|
|
||||||
|
err := enc.Encode(events)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = g.Close()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := target.client.R().
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetHeader("Content-Encoding", "gzip").
|
||||||
|
SetBody(buf).
|
||||||
|
Post("")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed write to event target: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.IsError() {
|
||||||
|
log.Printf("failed write to event target: %d %s: %s", resp.StatusCode(), resp.Status(), resp.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
36
event.go
36
event.go
@@ -1 +1,37 @@
|
|||||||
package event
|
package event
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
start time.Time
|
||||||
|
|
||||||
|
Time string `json:"time"`
|
||||||
|
SampleRate int64 `json:"samplerate"`
|
||||||
|
Data map[string]any `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEvent(eventType string, vals ...any) *Event {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
ev := &Event{
|
||||||
|
start: now,
|
||||||
|
Time: now.Format(time.RFC3339Nano),
|
||||||
|
Data: map[string]any{
|
||||||
|
"type": eventType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.Set(vals...)
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ev *Event) Set(vals ...any) {
|
||||||
|
if len(vals)%2 != 0 {
|
||||||
|
panic(vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(vals); i += 2 {
|
||||||
|
ev.Data[vals[i].(string)] = vals[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -1,3 +1,7 @@
|
|||||||
module github.com/gopatchy/event
|
module github.com/gopatchy/event
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
|
require github.com/go-resty/resty/v2 v2.7.0
|
||||||
|
|
||||||
|
require golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
|
||||||
|
|||||||
9
go.sum
Normal file
9
go.sum
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
|
||||||
|
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
|
||||||
|
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
|
||||||
|
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
84
hooks.go
Normal file
84
hooks.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime/debug"
|
||||||
|
"runtime/metrics"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HookBuildInfo(_ context.Context, ev *Event) {
|
||||||
|
buildInfo, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
panic("ReadBuildInfo() failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.Set(
|
||||||
|
"goVersion", buildInfo.GoVersion,
|
||||||
|
"goPackagePath", buildInfo.Path,
|
||||||
|
"goMainModuleVersion", buildInfo.Main.Version,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HookMetrics(_ context.Context, ev *Event) {
|
||||||
|
descs := metrics.All()
|
||||||
|
|
||||||
|
samples := make([]metrics.Sample, len(descs))
|
||||||
|
for i := range samples {
|
||||||
|
samples[i].Name = descs[i].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.Read(samples)
|
||||||
|
|
||||||
|
for _, sample := range samples {
|
||||||
|
name := convertMetricName(sample.Name)
|
||||||
|
|
||||||
|
switch sample.Value.Kind() { //nolint:exhaustive
|
||||||
|
case metrics.KindUint64:
|
||||||
|
ev.Set(name, sample.Value.Uint64())
|
||||||
|
case metrics.KindFloat64:
|
||||||
|
ev.Set(name, sample.Value.Float64())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HookRUsage(_ context.Context, ev *Event) {
|
||||||
|
rusage := &syscall.Rusage{}
|
||||||
|
|
||||||
|
err := syscall.Getrusage(syscall.RUSAGE_SELF, rusage)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.Set(
|
||||||
|
"rUsageUTime", time.Duration(rusage.Utime.Nano()).Seconds(),
|
||||||
|
"rUsageSTime", time.Duration(rusage.Stime.Nano()).Seconds(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertMetricName(in string) string {
|
||||||
|
upperNext := false
|
||||||
|
|
||||||
|
in = strings.TrimLeft(in, "/")
|
||||||
|
|
||||||
|
ret := strings.Builder{}
|
||||||
|
|
||||||
|
for _, r := range in {
|
||||||
|
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
|
||||||
|
upperNext = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if upperNext {
|
||||||
|
r = unicode.ToUpper(r)
|
||||||
|
upperNext = false
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.WriteRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret.String()
|
||||||
|
}
|
||||||
18
rateclass.go
Normal file
18
rateclass.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
type rateClass struct {
|
||||||
|
grantRate float64
|
||||||
|
criteria map[string]any
|
||||||
|
|
||||||
|
eventRate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *rateClass) match(ev *Event) bool {
|
||||||
|
for k, v := range rc.criteria {
|
||||||
|
if ev.Data[k] != v {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
70
target.go
Normal file
70
target.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Target struct {
|
||||||
|
client *resty.Client
|
||||||
|
writePeriodSeconds float64
|
||||||
|
windowSeconds float64
|
||||||
|
rateClasses []*rateClass
|
||||||
|
stop chan bool
|
||||||
|
lastEvent time.Time
|
||||||
|
events []*Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target *Target) AddRateClass(grantRate float64, vals ...any) {
|
||||||
|
if len(vals)%2 != 0 {
|
||||||
|
panic(vals)
|
||||||
|
}
|
||||||
|
|
||||||
|
erc := &rateClass{
|
||||||
|
grantRate: grantRate * target.windowSeconds,
|
||||||
|
criteria: map[string]any{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(vals); i += 2 {
|
||||||
|
erc.criteria[vals[i].(string)] = vals[i+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
target.rateClasses = append(target.rateClasses, erc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (target *Target) writeEvent(ev *Event) {
|
||||||
|
now := time.Now()
|
||||||
|
secondsSinceLastEvent := now.Sub(target.lastEvent).Seconds()
|
||||||
|
target.lastEvent = now
|
||||||
|
|
||||||
|
// Example:
|
||||||
|
// windowSeconds = 100
|
||||||
|
// secondsSinceLastEvent = 25
|
||||||
|
// eventRateMultiplier = 0.75
|
||||||
|
eventRateMultiplier := (target.windowSeconds - secondsSinceLastEvent) / target.windowSeconds
|
||||||
|
|
||||||
|
maxProb := 0.0
|
||||||
|
|
||||||
|
for _, erc := range target.rateClasses {
|
||||||
|
if !erc.match(ev) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
erc.eventRate++
|
||||||
|
erc.eventRate *= eventRateMultiplier
|
||||||
|
|
||||||
|
classProb := erc.grantRate / erc.eventRate
|
||||||
|
maxProb = math.Max(maxProb, classProb)
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxProb <= 0.0 || rand.Float64() > maxProb { //nolint:gosec
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ev2 := *ev
|
||||||
|
ev2.SampleRate = int64(math.Max(math.Round(1.0/maxProb), 1.0))
|
||||||
|
target.events = append(target.events, &ev2)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user