netdata/netdata

View on GitHub
src/go/collectors/go.d.plugin/modules/weblog/charts.go

Summary

Maintainability
F
4 days
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package weblog

import (
    "errors"
    "fmt"

    "github.com/netdata/netdata/go/go.d.plugin/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
}