src/go/plugin/go.d/modules/weblog/charts.go
// SPDX-License-Identifier: GPL-3.0-or-later
package weblog
import (
"errors"
"fmt"
"github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
)
type (
Charts = module.Charts
Chart = module.Chart
Dims = module.Dims
Dim = module.Dim
)
const (
prioReqTotal = module.Priority + iota
prioReqExcluded
prioReqType
prioRespCodesClass
prioRespCodes
prioRespCodes1xx
prioRespCodes2xx
prioRespCodes3xx
prioRespCodes4xx
prioRespCodes5xx
prioBandwidth
prioReqProcTime
prioRespTimeHist
prioUpsRespTime
prioUpsRespTimeHist
prioUniqIP
prioReqVhost
prioReqPort
prioReqScheme
prioReqMethod
prioReqVersion
prioReqIPProto
prioReqSSLProto
prioReqSSLCipherSuite
prioReqCustomFieldPattern // chart per custom field, alphabetical order
prioReqCustomTimeField // chart per custom time field, alphabetical order
prioReqCustomTimeFieldHist // histogram chart per custom time field
prioReqURLPattern
prioURLPatternStats
prioReqCustomNumericFieldSummary // 3 charts per url pattern, alphabetical order
)
// NOTE: inconsistency with python web_log
// TODO: current histogram charts are misleading in netdata
// Requests
var (
reqTotal = Chart{
ID: "requests",
Title: "Total Requests",
Units: "requests/s",
Fam: "requests",
Ctx: "web_log.requests",
Priority: prioReqTotal,
Dims: Dims{
{ID: "requests", Algo: module.Incremental},
},
}
reqExcluded = Chart{
ID: "excluded_requests",
Title: "Excluded Requests",
Units: "requests/s",
Fam: "requests",
Ctx: "web_log.excluded_requests",
Type: module.Stacked,
Priority: prioReqExcluded,
Dims: Dims{
{ID: "req_unmatched", Name: "unmatched", Algo: module.Incremental},
},
}
// netdata specific grouping
reqTypes = Chart{
ID: "requests_by_type",
Title: "Requests By Type",
Units: "requests/s",
Fam: "requests",
Ctx: "web_log.type_requests",
Type: module.Stacked,
Priority: prioReqType,
Dims: Dims{
{ID: "req_type_success", Name: "success", Algo: module.Incremental},
{ID: "req_type_bad", Name: "bad", Algo: module.Incremental},
{ID: "req_type_redirect", Name: "redirect", Algo: module.Incremental},
{ID: "req_type_error", Name: "error", Algo: module.Incremental},
},
}
)
// Responses
var (
respCodeClass = Chart{
ID: "responses_by_status_code_class",
Title: "Responses By Status Code Class",
Units: "responses/s",
Fam: "responses",
Ctx: "web_log.status_code_class_responses",
Type: module.Stacked,
Priority: prioRespCodesClass,
Dims: Dims{
{ID: "resp_2xx", Name: "2xx", Algo: module.Incremental},
{ID: "resp_5xx", Name: "5xx", Algo: module.Incremental},
{ID: "resp_3xx", Name: "3xx", Algo: module.Incremental},
{ID: "resp_4xx", Name: "4xx", Algo: module.Incremental},
{ID: "resp_1xx", Name: "1xx", Algo: module.Incremental},
},
}
respCodes = Chart{
ID: "responses_by_status_code",
Title: "Responses By Status Code",
Units: "responses/s",
Fam: "responses",
Ctx: "web_log.status_code_responses",
Type: module.Stacked,
Priority: prioRespCodes,
}
respCodes1xx = Chart{
ID: "status_code_class_1xx_responses",
Title: "Informational Responses By Status Code",
Units: "responses/s",
Fam: "responses",
Ctx: "web_log.status_code_class_1xx_responses",
Type: module.Stacked,
Priority: prioRespCodes1xx,
}
respCodes2xx = Chart{
ID: "status_code_class_2xx_responses",
Title: "Successful Responses By Status Code",
Units: "responses/s",
Fam: "responses",
Ctx: "web_log.status_code_class_2xx_responses",
Type: module.Stacked,
Priority: prioRespCodes2xx,
}
respCodes3xx = Chart{
ID: "status_code_class_3xx_responses",
Title: "Redirects Responses By Status Code",
Units: "responses/s",
Fam: "responses",
Ctx: "web_log.status_code_class_3xx_responses",
Type: module.Stacked,
Priority: prioRespCodes3xx,
}
respCodes4xx = Chart{
ID: "status_code_class_4xx_responses",
Title: "Client Errors Responses By Status Code",
Units: "responses/s",
Fam: "responses",
Ctx: "web_log.status_code_class_4xx_responses",
Type: module.Stacked,
Priority: prioRespCodes4xx,
}
respCodes5xx = Chart{
ID: "status_code_class_5xx_responses",
Title: "Server Errors Responses By Status Code",
Units: "responses/s",
Fam: "responses",
Ctx: "web_log.status_code_class_5xx_responses",
Type: module.Stacked,
Priority: prioRespCodes5xx,
}
)
// Bandwidth
var (
bandwidth = Chart{
ID: "bandwidth",
Title: "Bandwidth",
Units: "kilobits/s",
Fam: "bandwidth",
Ctx: "web_log.bandwidth",
Type: module.Area,
Priority: prioBandwidth,
Dims: Dims{
{ID: "bytes_received", Name: "received", Algo: module.Incremental, Mul: 8, Div: 1000},
{ID: "bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -8, Div: 1000},
},
}
)
// Timings
var (
reqProcTime = Chart{
ID: "request_processing_time",
Title: "Request Processing Time",
Units: "milliseconds",
Fam: "timings",
Ctx: "web_log.request_processing_time",
Priority: prioReqProcTime,
Dims: Dims{
{ID: "req_proc_time_min", Name: "min", Div: 1000},
{ID: "req_proc_time_max", Name: "max", Div: 1000},
{ID: "req_proc_time_avg", Name: "avg", Div: 1000},
},
}
reqProcTimeHist = Chart{
ID: "requests_processing_time_histogram",
Title: "Requests Processing Time Histogram",
Units: "requests/s",
Fam: "timings",
Ctx: "web_log.requests_processing_time_histogram",
Priority: prioRespTimeHist,
}
)
// Upstream
var (
upsRespTime = Chart{
ID: "upstream_response_time",
Title: "Upstream Response Time",
Units: "milliseconds",
Fam: "timings",
Ctx: "web_log.upstream_response_time",
Priority: prioUpsRespTime,
Dims: Dims{
{ID: "upstream_resp_time_min", Name: "min", Div: 1000},
{ID: "upstream_resp_time_max", Name: "max", Div: 1000},
{ID: "upstream_resp_time_avg", Name: "avg", Div: 1000},
},
}
upsRespTimeHist = Chart{
ID: "upstream_responses_time_histogram",
Title: "Upstream Responses Time Histogram",
Units: "responses/s",
Fam: "timings",
Ctx: "web_log.upstream_responses_time_histogram",
Priority: prioUpsRespTimeHist,
}
)
// Clients
var (
uniqIPsCurPoll = Chart{
ID: "current_poll_uniq_clients",
Title: "Current Poll Unique Clients",
Units: "clients",
Fam: "client",
Ctx: "web_log.current_poll_uniq_clients",
Type: module.Stacked,
Priority: prioUniqIP,
Dims: Dims{
{ID: "uniq_ipv4", Name: "ipv4", Algo: module.Absolute},
{ID: "uniq_ipv6", Name: "ipv6", Algo: module.Absolute},
},
}
)
// Request By N
var (
reqByVhost = Chart{
ID: "requests_by_vhost",
Title: "Requests By Vhost",
Units: "requests/s",
Fam: "vhost",
Ctx: "web_log.vhost_requests",
Type: module.Stacked,
Priority: prioReqVhost,
}
reqByPort = Chart{
ID: "requests_by_port",
Title: "Requests By Port",
Units: "requests/s",
Fam: "port",
Ctx: "web_log.port_requests",
Type: module.Stacked,
Priority: prioReqPort,
}
reqByScheme = Chart{
ID: "requests_by_scheme",
Title: "Requests By Scheme",
Units: "requests/s",
Fam: "scheme",
Ctx: "web_log.scheme_requests",
Type: module.Stacked,
Priority: prioReqScheme,
Dims: Dims{
{ID: "req_http_scheme", Name: "http", Algo: module.Incremental},
{ID: "req_https_scheme", Name: "https", Algo: module.Incremental},
},
}
reqByMethod = Chart{
ID: "requests_by_http_method",
Title: "Requests By HTTP Method",
Units: "requests/s",
Fam: "http method",
Ctx: "web_log.http_method_requests",
Type: module.Stacked,
Priority: prioReqMethod,
}
reqByVersion = Chart{
ID: "requests_by_http_version",
Title: "Requests By HTTP Version",
Units: "requests/s",
Fam: "http version",
Ctx: "web_log.http_version_requests",
Type: module.Stacked,
Priority: prioReqVersion,
}
reqByIPProto = Chart{
ID: "requests_by_ip_proto",
Title: "Requests By IP Protocol",
Units: "requests/s",
Fam: "ip proto",
Ctx: "web_log.ip_proto_requests",
Type: module.Stacked,
Priority: prioReqIPProto,
Dims: Dims{
{ID: "req_ipv4", Name: "ipv4", Algo: module.Incremental},
{ID: "req_ipv6", Name: "ipv6", Algo: module.Incremental},
},
}
reqBySSLProto = Chart{
ID: "requests_by_ssl_proto",
Title: "Requests By SSL Connection Protocol",
Units: "requests/s",
Fam: "ssl conn",
Ctx: "web_log.ssl_proto_requests",
Type: module.Stacked,
Priority: prioReqSSLProto,
}
reqBySSLCipherSuite = Chart{
ID: "requests_by_ssl_cipher_suite",
Title: "Requests By SSL Connection Cipher Suite",
Units: "requests/s",
Fam: "ssl conn",
Ctx: "web_log.ssl_cipher_suite_requests",
Type: module.Stacked,
Priority: prioReqSSLCipherSuite,
}
)
// Request By N Patterns
var (
reqByURLPattern = Chart{
ID: "requests_by_url_pattern",
Title: "URL Field Requests By Pattern",
Units: "requests/s",
Fam: "url ptn",
Ctx: "web_log.url_pattern_requests",
Type: module.Stacked,
Priority: prioReqURLPattern,
}
reqByCustomFieldPattern = Chart{
ID: "custom_field_%s_requests_by_pattern",
Title: "Custom Field %s Requests By Pattern",
Units: "requests/s",
Fam: "custom field ptn",
Ctx: "web_log.custom_field_pattern_requests",
Type: module.Stacked,
Priority: prioReqCustomFieldPattern,
}
)
// custom time field
var (
reqByCustomTimeField = Chart{
ID: "custom_time_field_%s_summary",
Title: `Custom Time Field "%s" Summary`,
Units: "milliseconds",
Fam: "custom time field",
Ctx: "web_log.custom_time_field_summary",
Priority: prioReqCustomTimeField,
Dims: Dims{
{ID: "custom_time_field_%s_time_min", Name: "min", Div: 1000},
{ID: "custom_time_field_%s_time_max", Name: "max", Div: 1000},
{ID: "custom_time_field_%s_time_avg", Name: "avg", Div: 1000},
},
}
reqByCustomTimeFieldHist = Chart{
ID: "custom_time_field_%s_histogram",
Title: `Custom Time Field "%s" Histogram`,
Units: "observations",
Fam: "custom time field",
Ctx: "web_log.custom_time_field_histogram",
Priority: prioReqCustomTimeFieldHist,
}
)
var (
customNumericFieldSummaryChartTmpl = Chart{
ID: "custom_numeric_field_%s_summary",
Title: "Custom Numeric Field Summary",
Units: "",
Fam: "custom numeric fields",
Ctx: "web_log.custom_numeric_field_%s_summary",
Priority: prioReqCustomNumericFieldSummary,
Dims: Dims{
{ID: "custom_numeric_field_%s_summary_min", Name: "min"},
{ID: "custom_numeric_field_%s_summary_max", Name: "max"},
{ID: "custom_numeric_field_%s_summary_avg", Name: "avg"},
},
}
)
// URL pattern stats
var (
urlPatternRespCodes = Chart{
ID: "url_pattern_%s_responses_by_status_code",
Title: "Responses By Status Code",
Units: "responses/s",
Fam: "url ptn %s",
Ctx: "web_log.url_pattern_status_code_responses",
Type: module.Stacked,
Priority: prioURLPatternStats,
}
urlPatternReqMethods = Chart{
ID: "url_pattern_%s_requests_by_http_method",
Title: "Requests By HTTP Method",
Units: "requests/s",
Fam: "url ptn %s",
Ctx: "web_log.url_pattern_http_method_requests",
Type: module.Stacked,
Priority: prioURLPatternStats + 1,
}
urlPatternBandwidth = Chart{
ID: "url_pattern_%s_bandwidth",
Title: "Bandwidth",
Units: "kilobits/s",
Fam: "url ptn %s",
Ctx: "web_log.url_pattern_bandwidth",
Type: module.Area,
Priority: prioURLPatternStats + 2,
Dims: Dims{
{ID: "url_ptn_%s_bytes_received", Name: "received", Algo: module.Incremental, Mul: 8, Div: 1000},
{ID: "url_ptn_%s_bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -8, Div: 1000},
},
}
urlPatternReqProcTime = Chart{
ID: "url_pattern_%s_request_processing_time",
Title: "Request Processing Time",
Units: "milliseconds",
Fam: "url ptn %s",
Ctx: "web_log.url_pattern_request_processing_time",
Priority: prioURLPatternStats + 3,
Dims: Dims{
{ID: "url_ptn_%s_req_proc_time_min", Name: "min", Div: 1000},
{ID: "url_ptn_%s_req_proc_time_max", Name: "max", Div: 1000},
{ID: "url_ptn_%s_req_proc_time_avg", Name: "avg", Div: 1000},
},
}
)
func newReqProcTimeHistChart(histogram []float64) (*Chart, error) {
chart := reqProcTimeHist.Copy()
for i, v := range histogram {
dim := &Dim{
ID: fmt.Sprintf("req_proc_time_hist_bucket_%d", i+1),
Name: fmt.Sprintf("%.3f", v),
Algo: module.Incremental,
}
if err := chart.AddDim(dim); err != nil {
return nil, err
}
}
if err := chart.AddDim(&Dim{
ID: "req_proc_time_hist_count",
Name: "+Inf",
Algo: module.Incremental,
}); err != nil {
return nil, err
}
return chart, nil
}
func newUpsRespTimeHistChart(histogram []float64) (*Chart, error) {
chart := upsRespTimeHist.Copy()
for i, v := range histogram {
dim := &Dim{
ID: fmt.Sprintf("upstream_resp_time_hist_bucket_%d", i+1),
Name: fmt.Sprintf("%.3f", v),
Algo: module.Incremental,
}
if err := chart.AddDim(dim); err != nil {
return nil, err
}
}
if err := chart.AddDim(&Dim{
ID: "upstream_resp_time_hist_count",
Name: "+Inf",
Algo: module.Incremental,
}); err != nil {
return nil, err
}
return chart, nil
}
func newURLPatternChart(patterns []userPattern) (*Chart, error) {
chart := reqByURLPattern.Copy()
for _, p := range patterns {
dim := &Dim{
ID: "req_url_ptn_" + p.Name,
Name: p.Name,
Algo: module.Incremental,
}
if err := chart.AddDim(dim); err != nil {
return nil, err
}
}
return chart, nil
}
func newURLPatternRespCodesChart(name string) *Chart {
chart := urlPatternRespCodes.Copy()
chart.ID = fmt.Sprintf(chart.ID, name)
chart.Fam = fmt.Sprintf(chart.Fam, name)
return chart
}
func newURLPatternReqMethodsChart(name string) *Chart {
chart := urlPatternReqMethods.Copy()
chart.ID = fmt.Sprintf(chart.ID, name)
chart.Fam = fmt.Sprintf(chart.Fam, name)
return chart
}
func newURLPatternBandwidthChart(name string) *Chart {
chart := urlPatternBandwidth.Copy()
chart.ID = fmt.Sprintf(chart.ID, name)
chart.Fam = fmt.Sprintf(chart.Fam, name)
for _, d := range chart.Dims {
d.ID = fmt.Sprintf(d.ID, name)
}
return chart
}
func newURLPatternReqProcTimeChart(name string) *Chart {
chart := urlPatternReqProcTime.Copy()
chart.ID = fmt.Sprintf(chart.ID, name)
chart.Fam = fmt.Sprintf(chart.Fam, name)
for _, d := range chart.Dims {
d.ID = fmt.Sprintf(d.ID, name)
}
return chart
}
func newCustomFieldCharts(fields []customField) (Charts, error) {
charts := Charts{}
for _, f := range fields {
chart, err := newCustomFieldChart(f)
if err != nil {
return nil, err
}
if err := charts.Add(chart); err != nil {
return nil, err
}
}
return charts, nil
}
func newCustomFieldChart(f customField) (*Chart, error) {
chart := reqByCustomFieldPattern.Copy()
chart.ID = fmt.Sprintf(chart.ID, f.Name)
chart.Title = fmt.Sprintf(chart.Title, f.Name)
for _, p := range f.Patterns {
dim := &Dim{
ID: fmt.Sprintf("custom_field_%s_%s", f.Name, p.Name),
Name: p.Name,
Algo: module.Incremental,
}
if err := chart.AddDim(dim); err != nil {
return nil, err
}
}
return chart, nil
}
func newCustomTimeFieldCharts(fields []customTimeField) (Charts, error) {
charts := Charts{}
for i, f := range fields {
chartTime, err := newCustomTimeFieldChart(f)
if err != nil {
return nil, err
}
chartTime.Priority += i
if err := charts.Add(chartTime); err != nil {
return nil, err
}
if len(f.Histogram) < 1 {
continue
}
chartHist, err := newCustomTimeFieldHistChart(f)
if err != nil {
return nil, err
}
chartHist.Priority += i
if err := charts.Add(chartHist); err != nil {
return nil, err
}
}
return charts, nil
}
func newCustomTimeFieldChart(f customTimeField) (*Chart, error) {
chart := reqByCustomTimeField.Copy()
chart.ID = fmt.Sprintf(chart.ID, f.Name)
chart.Title = fmt.Sprintf(chart.Title, f.Name)
for _, d := range chart.Dims {
d.ID = fmt.Sprintf(d.ID, f.Name)
}
return chart, nil
}
func newCustomTimeFieldHistChart(f customTimeField) (*Chart, error) {
chart := reqByCustomTimeFieldHist.Copy()
chart.ID = fmt.Sprintf(chart.ID, f.Name)
chart.Title = fmt.Sprintf(chart.Title, f.Name)
for i, v := range f.Histogram {
dim := &Dim{
ID: fmt.Sprintf("custom_time_field_%s_time_hist_bucket_%d", f.Name, i+1),
Name: fmt.Sprintf("%.3f", v),
Algo: module.Incremental,
}
if err := chart.AddDim(dim); err != nil {
return nil, err
}
}
if err := chart.AddDim(&Dim{
ID: fmt.Sprintf("custom_time_field_%s_time_hist_count", f.Name),
Name: "+Inf",
Algo: module.Incremental,
}); err != nil {
return nil, err
}
return chart, nil
}
func (w *WebLog) createCharts(line *logLine) error {
if line.empty() {
return errors.New("empty line")
}
w.charts = nil
// Following charts are created during runtime:
// - reqBySSLProto, reqBySSLCipherSuite - it is likely line has no SSL stuff at this moment
charts := &Charts{
reqTotal.Copy(),
reqExcluded.Copy(),
}
if line.hasVhost() {
if err := addVhostCharts(charts); err != nil {
return err
}
}
if line.hasPort() {
if err := addPortCharts(charts); err != nil {
return err
}
}
if line.hasReqScheme() {
if err := addSchemeCharts(charts); err != nil {
return err
}
}
if line.hasReqClient() {
if err := addClientCharts(charts); err != nil {
return err
}
}
if line.hasReqMethod() {
if err := addMethodCharts(charts, w.URLPatterns); err != nil {
return err
}
}
if line.hasReqURL() {
if err := addURLCharts(charts, w.URLPatterns); err != nil {
return err
}
}
if line.hasReqProto() {
if err := addReqProtoCharts(charts); err != nil {
return err
}
}
if line.hasRespCode() {
if err := addRespCodesCharts(charts, w.GroupRespCodes); err != nil {
return err
}
}
if line.hasReqSize() || line.hasRespSize() {
if err := addBandwidthCharts(charts, w.URLPatterns); err != nil {
return err
}
}
if line.hasReqProcTime() {
if err := addReqProcTimeCharts(charts, w.Histogram, w.URLPatterns); err != nil {
return err
}
}
if line.hasUpsRespTime() {
if err := addUpstreamRespTimeCharts(charts, w.Histogram); err != nil {
return err
}
}
if line.hasCustomFields() {
if len(w.CustomFields) > 0 {
if err := addCustomFieldsCharts(charts, w.CustomFields); err != nil {
return err
}
}
if len(w.CustomTimeFields) > 0 {
if err := addCustomTimeFieldsCharts(charts, w.CustomTimeFields); err != nil {
return err
}
}
if len(w.CustomNumericFields) > 0 {
if err := addCustomNumericFieldsCharts(charts, w.CustomNumericFields); err != nil {
return err
}
}
}
w.charts = charts
return nil
}
func addVhostCharts(charts *Charts) error {
return charts.Add(reqByVhost.Copy())
}
func addPortCharts(charts *Charts) error {
return charts.Add(reqByPort.Copy())
}
func addSchemeCharts(charts *Charts) error {
return charts.Add(reqByScheme.Copy())
}
func addClientCharts(charts *Charts) error {
if err := charts.Add(reqByIPProto.Copy()); err != nil {
return err
}
return charts.Add(uniqIPsCurPoll.Copy())
}
func addMethodCharts(charts *Charts, patterns []userPattern) error {
if err := charts.Add(reqByMethod.Copy()); err != nil {
return err
}
for _, p := range patterns {
chart := newURLPatternReqMethodsChart(p.Name)
if err := charts.Add(chart); err != nil {
return err
}
}
return nil
}
func addURLCharts(charts *Charts, patterns []userPattern) error {
if len(patterns) == 0 {
return nil
}
chart, err := newURLPatternChart(patterns)
if err != nil {
return err
}
if err := charts.Add(chart); err != nil {
return err
}
for _, p := range patterns {
chart := newURLPatternRespCodesChart(p.Name)
if err := charts.Add(chart); err != nil {
return err
}
}
return nil
}
func addReqProtoCharts(charts *Charts) error {
return charts.Add(reqByVersion.Copy())
}
func addRespCodesCharts(charts *Charts, group bool) error {
if err := charts.Add(reqTypes.Copy()); err != nil {
return err
}
if err := charts.Add(respCodeClass.Copy()); err != nil {
return err
}
if !group {
return charts.Add(respCodes.Copy())
}
for _, c := range []Chart{respCodes1xx, respCodes2xx, respCodes3xx, respCodes4xx, respCodes5xx} {
if err := charts.Add(c.Copy()); err != nil {
return err
}
}
return nil
}
func addBandwidthCharts(charts *Charts, patterns []userPattern) error {
if err := charts.Add(bandwidth.Copy()); err != nil {
return err
}
for _, p := range patterns {
chart := newURLPatternBandwidthChart(p.Name)
if err := charts.Add(chart); err != nil {
return err
}
}
return nil
}
func addReqProcTimeCharts(charts *Charts, histogram []float64, patterns []userPattern) error {
if err := charts.Add(reqProcTime.Copy()); err != nil {
return err
}
for _, p := range patterns {
chart := newURLPatternReqProcTimeChart(p.Name)
if err := charts.Add(chart); err != nil {
return err
}
}
if len(histogram) == 0 {
return nil
}
chart, err := newReqProcTimeHistChart(histogram)
if err != nil {
return err
}
return charts.Add(chart)
}
func addUpstreamRespTimeCharts(charts *Charts, histogram []float64) error {
if err := charts.Add(upsRespTime.Copy()); err != nil {
return err
}
if len(histogram) == 0 {
return nil
}
chart, err := newUpsRespTimeHistChart(histogram)
if err != nil {
return err
}
return charts.Add(chart)
}
func addCustomFieldsCharts(charts *Charts, fields []customField) error {
cs, err := newCustomFieldCharts(fields)
if err != nil {
return err
}
return charts.Add(cs...)
}
func addCustomTimeFieldsCharts(charts *Charts, fields []customTimeField) error {
cs, err := newCustomTimeFieldCharts(fields)
if err != nil {
return err
}
return charts.Add(cs...)
}
func addCustomNumericFieldsCharts(charts *module.Charts, fields []customNumericField) error {
for _, f := range fields {
chart := customNumericFieldSummaryChartTmpl.Copy()
chart.ID = fmt.Sprintf(chart.ID, f.Name)
chart.Units = f.Units
chart.Ctx = fmt.Sprintf(chart.Ctx, f.Name)
for _, dim := range chart.Dims {
dim.ID = fmt.Sprintf(dim.ID, f.Name)
dim.Div = f.Divisor
}
if err := charts.Add(chart); err != nil {
return err
}
}
return nil
}