ml
This commit is contained in:
1 parent
21f04d696b
commit
8f5c267444
26 files changed
+3039
-10
No files matched your search
@@ -0,0 +1,240 @@
|
||||
// issue-model trains and queries an explainable prototype model for normalized
|
||||
// events. It uses deterministic pattern signatures as weak labels until human
|
||||
// labels are available.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type event struct {
|
||||
EventID string `json:"eventId"`
|
||||
Service string `json:"service"`
|
||||
Kind string `json:"kind"`
|
||||
Source string `json:"source"`
|
||||
IsError uint8 `json:"isError"`
|
||||
StatusClass string `json:"statusClass,omitempty"`
|
||||
StatusCode uint16 `json:"statusCode,omitempty"`
|
||||
Route string `json:"route,omitempty"`
|
||||
SpanName string `json:"spanName,omitempty"`
|
||||
MetricName string `json:"metricName,omitempty"`
|
||||
MetricType string `json:"metricType,omitempty"`
|
||||
ExceptionType string `json:"exceptionType,omitempty"`
|
||||
Message string `json:"messageNormalized,omitempty"`
|
||||
StackSignature string `json:"stackSignature,omitempty"`
|
||||
PatternSignature string `json:"patternSignature"`
|
||||
}
|
||||
|
||||
type prototype struct {
|
||||
Pattern string `json:"patternSignature"`
|
||||
Count int `json:"count"`
|
||||
Tokens map[string]float64 `json:"tokens"`
|
||||
}
|
||||
|
||||
type model struct {
|
||||
Version string `json:"version"`
|
||||
FeatureSpec string `json:"featureSpec"`
|
||||
Prototypes []prototype `json:"prototypes"`
|
||||
}
|
||||
|
||||
type prediction struct {
|
||||
EventID string `json:"eventId"`
|
||||
ActualPattern string `json:"actualPattern,omitempty"`
|
||||
Pattern string `json:"patternSignature"`
|
||||
Score float64 `json:"score"`
|
||||
Matches []string `json:"matches"`
|
||||
Rank int `json:"rank"`
|
||||
Correct bool `json:"correct,omitempty"`
|
||||
Accepted bool `json:"accepted"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
trainPath := flag.String("train", "", "JSONL training sample")
|
||||
modelPath := flag.String("model", "/tmp/issue-model-v001.json", "model path")
|
||||
predictPath := flag.String("predict", "", "JSONL events to score")
|
||||
top := flag.Int("top", 3, "number of predictions per event")
|
||||
minScore := flag.Float64("min-score", 0.75, "minimum top similarity to accept a match")
|
||||
minMargin := flag.Float64("min-margin", 0.05, "minimum gap between first and second match")
|
||||
flag.Parse()
|
||||
if *trainPath == "" && *predictPath == "" {
|
||||
log.Fatal("provide -train or -predict")
|
||||
}
|
||||
if *trainPath != "" {
|
||||
trained := train(*trainPath)
|
||||
writeJSON(*modelPath, trained)
|
||||
fmt.Printf("trained prototypes=%d model=%s\n", len(trained.Prototypes), *modelPath)
|
||||
}
|
||||
if *predictPath != "" {
|
||||
var trained model
|
||||
readJSON(*modelPath, &trained)
|
||||
predict(*predictPath, trained, *top, *minScore, *minMargin)
|
||||
}
|
||||
}
|
||||
|
||||
func train(path string) model {
|
||||
groups := map[string]*prototype{}
|
||||
forEachEvent(path, func(item event) {
|
||||
if item.PatternSignature == "" {
|
||||
return
|
||||
}
|
||||
p := groups[item.PatternSignature]
|
||||
if p == nil {
|
||||
p = &prototype{Pattern: item.PatternSignature, Tokens: map[string]float64{}}
|
||||
groups[item.PatternSignature] = p
|
||||
}
|
||||
p.Count++
|
||||
for token := range features(item) {
|
||||
p.Tokens[token]++
|
||||
}
|
||||
})
|
||||
result := model{Version: "v1", FeatureSpec: "categorical tokens excluding IDs, timestamps, trace IDs, IPs, and raw values"}
|
||||
for _, p := range groups {
|
||||
for token, value := range p.Tokens {
|
||||
p.Tokens[token] = value / float64(p.Count)
|
||||
}
|
||||
result.Prototypes = append(result.Prototypes, *p)
|
||||
}
|
||||
sort.Slice(result.Prototypes, func(i, j int) bool { return result.Prototypes[i].Pattern < result.Prototypes[j].Pattern })
|
||||
return result
|
||||
}
|
||||
|
||||
func predict(path string, trained model, top int, minScore, minMargin float64) {
|
||||
if top < 1 {
|
||||
top = 1
|
||||
}
|
||||
if len(trained.Prototypes) == 0 {
|
||||
log.Fatal("model contains no prototypes; train it with a non-empty normalized sample")
|
||||
}
|
||||
forEachEvent(path, func(item event) {
|
||||
input := features(item)
|
||||
results := make([]prediction, 0, len(trained.Prototypes))
|
||||
for _, p := range trained.Prototypes {
|
||||
score, matches := similarity(input, p.Tokens)
|
||||
results = append(results, prediction{EventID: item.EventID, Pattern: p.Pattern, Score: score, Matches: matches})
|
||||
}
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].Score != results[j].Score {
|
||||
return results[i].Score > results[j].Score
|
||||
}
|
||||
return results[i].Pattern < results[j].Pattern
|
||||
})
|
||||
if len(results) > top {
|
||||
results = results[:top]
|
||||
}
|
||||
margin := results[0].Score
|
||||
if len(results) > 1 {
|
||||
margin -= results[1].Score
|
||||
}
|
||||
for i := range results {
|
||||
results[i].ActualPattern = item.PatternSignature
|
||||
results[i].Rank = i + 1
|
||||
results[i].Correct = results[i].Pattern == item.PatternSignature
|
||||
results[i].Accepted = results[0].Score >= minScore && margin >= minMargin
|
||||
}
|
||||
for _, result := range results {
|
||||
writeLine(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func features(item event) map[string]struct{} {
|
||||
result := map[string]struct{}{}
|
||||
add := func(key, value string) {
|
||||
if value != "" {
|
||||
result[key+"="+value] = struct{}{}
|
||||
}
|
||||
}
|
||||
add("service", item.Service)
|
||||
add("kind", item.Kind)
|
||||
add("source", item.Source)
|
||||
add("status", item.StatusClass)
|
||||
if item.StatusCode > 0 {
|
||||
add("statusCode", strconv.Itoa(int(item.StatusCode)))
|
||||
}
|
||||
add("route", item.Route)
|
||||
add("span", item.SpanName)
|
||||
add("metric", item.MetricName)
|
||||
add("metricType", item.MetricType)
|
||||
add("exception", item.ExceptionType)
|
||||
add("message", item.Message)
|
||||
add("stack", item.StackSignature)
|
||||
return result
|
||||
}
|
||||
|
||||
func similarity(input map[string]struct{}, prototype map[string]float64) (float64, []string) {
|
||||
if len(input) == 0 || len(prototype) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var dot, inputNorm, protoNorm float64
|
||||
matches := []string{}
|
||||
for token := range input {
|
||||
inputNorm++
|
||||
if weight, ok := prototype[token]; ok {
|
||||
dot += weight
|
||||
matches = append(matches, token)
|
||||
}
|
||||
}
|
||||
for _, weight := range prototype {
|
||||
protoNorm += weight * weight
|
||||
}
|
||||
if protoNorm == 0 {
|
||||
return 0, matches
|
||||
}
|
||||
score := dot / math.Sqrt(inputNorm*protoNorm)
|
||||
sort.Strings(matches)
|
||||
return score, matches
|
||||
}
|
||||
|
||||
func forEachEvent(path string, callback func(event)) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
|
||||
for scanner.Scan() {
|
||||
var item event
|
||||
if err := json.Unmarshal(scanner.Bytes(), &item); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
callback(item)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
func writeJSON(path string, value any) {
|
||||
data, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
func readJSON(path string, value any) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(data, value); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
func writeLine(value any) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println(strings.TrimSpace(string(data)))
|
||||
}
|
||||
Reference in new issue
Block a user