metricator/internal/application/parser.go

181 lines
4.6 KiB
Go

package application
import (
"strings"
"go.dev.pztrn.name/metricator/internal/models"
)
// Parses passed body and returns a map suitable for pushing into storage.
func (a *Application) parse(body string) map[string]models.Metric {
data := make(map[string]models.Metric)
// ToDo: switch to bytes buffer and maybe do not read body in caller?
splittedBody := strings.Split(body, "\n")
for _, line := range splittedBody {
// Prometheus line contains metric name and metric parameters defined
// in "{}".
var (
name string
params []string
)
// Skip empty lines.
if line == "" {
continue
}
a.logger.Debugln("Analyzing line:", line)
name = a.getMetricName(line)
metric, found := data[name]
if !found {
metric = models.NewMetric(name, "", "", nil)
}
// If line is commented - then we have something about metric's description
// or type. It should be handled in special way - these metric will became
// "pseudometric" which will be used as template for next iterations. For
// example if HELP line was parsed first, then TYPE line will be parsed and
// data will be added to already existing metric. If next line which should
// represent metric itself contains parameters (e.g. "{instance='host1'}")
// then next code block will COPY populated with HELP and TYPE pesudometric
// and put it's value as new metric with different name (metric/instance:host1
// in this example).
if strings.HasPrefix(line, "#") {
switch strings.Split(line, " ")[1] {
case "HELP":
metric.Description = a.getMetricDescription(line)
case "TYPE":
metric.Type = a.getMetricType(line)
}
data[name] = metric
// According to docs HELP and TYPE lines should be printed before actual metric. Do not even
// report bugs regarding that!
// Docs: https://github.com/Showmax/prometheus-docs/blob/master/content/docs/instrumenting/exposition_formats.md
continue
}
// Parametrized metrics should have own piece of love - we should
// add parameters to metric's name. This would also require metrics
// structure copying.
if strings.Contains(line, "{") {
newMetric := metric
params = a.getParametersForPrometheusMetric(line)
for _, param := range params {
newMetric.Name += "/" + param
}
metric = newMetric
data[metric.Name] = metric
}
metric.Value = a.getMetricValue(line)
a.logger.Debugf("Got metric: %+v\n", metric)
data[name] = metric
}
a.logger.Debugf("Data parsed: %+v\n", data)
return data
}
// Gets metric description from passed line.
func (a *Application) getMetricDescription(line string) string {
return strings.Join(strings.Split(line, " ")[3:], " ")
}
// Gets metric name from passed line.
func (a *Application) getMetricName(line string) string {
var metricNameData string
if strings.HasPrefix(line, "#") {
metricNameData = strings.Split(line, " ")[2]
} else {
metricNameData = strings.Split(line, " ")[0]
}
return strings.Split(metricNameData, "{")[0]
}
// Gets metric type from passed line.
func (a *Application) getMetricType(line string) string {
return strings.Split(line, " ")[3]
}
// Gets metric value from passed line.
func (a *Application) getMetricValue(line string) string {
if strings.Contains(line, "}") {
return strings.Split(line, "} ")[1]
}
return strings.Split(line, " ")[1]
}
// Parses passed line and returns a slice of strings with parameters parsed.
func (a *Application) getParametersForPrometheusMetric(line string) []string {
valuesString := strings.Split(strings.Split(line, "{")[1], "}")[0]
var (
params []string
paramName, paramValue string
paramNameFinished, paramValueStarted, paramValueFinished bool
)
for _, r := range valuesString {
if paramValueFinished && string(r) == "," {
params = append(params, paramName+":"+paramValue)
paramName, paramValue = "", ""
paramNameFinished, paramValueStarted, paramValueFinished = false, false, false
continue
}
// Sometimes nestif causes questions, like here. Is code below is
// "deeply nested"? I think not. So:
// nolint:nestif
if !paramNameFinished {
if string(r) != "=" {
paramName += string(r)
continue
} else {
paramNameFinished = true
continue
}
} else {
if string(r) == "\"" && !paramValueStarted {
paramValueStarted = true
continue
}
if paramValueStarted && string(r) != "\"" {
paramValue += string(r)
continue
}
if paramValueStarted && string(r) == "\"" {
paramValueFinished = true
continue
}
}
}
if paramName != "" && paramValue != "" {
params = append(params, paramName+":"+paramValue)
}
return params
}