package application import ( "bufio" "fmt" "io" "strings" "go.dev.pztrn.name/metricator/pkg/schema" ) // Parses io.Reader passed and returns a map suitable for pushing into storage. func (a *Application) parse(r io.Reader) (map[string]schema.Metric, error) { data := make(map[string]schema.Metric) scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() // Skip empty lines. if line == "" { continue } // Prometheus line contains metric name and metric parameters defined // in "{}". var ( name string params []string ) a.logger.Debugln("Analyzing line:", line) name = a.getMetricName(line) a.logger.Debugln("Got metric name:", name) metric, found := data[name] if !found { a.logger.Debugln("Metric wasn't yet created, creating new structure") metric = schema.NewMetric(name, "", "", nil) } a.logger.Debugf("Got metric to use: %+v\n", metric) // 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 newMetric.Name = newMetric.BaseName params = a.getParametersForPrometheusMetric(line) for _, param := range params { newMetric.Name += "/" + param } newMetric.Params = params metric = newMetric } metric.Value = a.getMetricValue(line) a.logger.Debugf("Got metric: %+v\n", metric) data[metric.Name] = metric } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("wasn't able to scan input: %w", err) } a.logger.Debugf("Data parsed: %+v\n", data) return data, nil } // 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 _, runeChar := range valuesString { if paramValueFinished && string(runeChar) == "," { 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(runeChar) != "=" { paramName += string(runeChar) continue } else { paramNameFinished = true continue } } else { if string(runeChar) == "\"" && !paramValueStarted { paramValueStarted = true continue } if paramValueStarted && string(runeChar) != "\"" { paramValue += string(runeChar) continue } if paramValueStarted && string(runeChar) == "\"" { paramValueFinished = true continue } } } if paramName != "" && paramValue != "" { params = append(params, paramName+":"+paramValue) } return params }