<file_sep>LiteSpeed Exporter golang code root library directory
----

This directory holds the golang libraries that belong to and are used by litespeed_exporter.
<file_sep>package collector

import (
	"github.com/prometheus/client_golang/prometheus"

	"github.com/myokoo/litespeed_exporter/pkg/rtreport"
)

var (
	networkLabel = []string{"scheme", "stream"}
	nName        = "network"
)

type network struct{}

func (n network) scrape(ch chan<- prometheus.Metric, report *rtreport.LiteSpeedReport) {
	for key, value := range report.NetworkReport {
		switch key {
		case rtreport.NetworkReportKeyBpsIn:
			ch <- newMetric(
				namespace, nName, "throughput",
				"Current ingress network throughput (http).",
				networkLabel, prometheus.GaugeValue, value, "http", "in",
			)
		case rtreport.NetworkReportKeyBpsOut:
			ch <- newMetric(
				namespace, nName, "throughput",
				"Current egress network throughput (http).",
				networkLabel, prometheus.GaugeValue, value, "http", "out",
			)
		case rtreport.NetworkReportKeySslBpsIn:
			ch <- newMetric(
				namespace, nName, "throughput",
				"Current ingress network throughput (https).",
				networkLabel, prometheus.GaugeValue, value, "https", "in",
			)
		case rtreport.NetworkReportKeySslBpsOut:
			ch <- newMetric(
				namespace, nName, "throughput",
				"Current egress network throughput (https).",
				networkLabel, prometheus.GaugeValue, value, "https", "out",
			)
		}
	}
}
<file_sep>package collector

import (
	"github.com/prometheus/client_golang/prometheus"

	"github.com/myokoo/litespeed_exporter/pkg/rtreport"
)

// Scraper is a minimal interface that allows you to add new prometheus metrics to litespeed_exporter.
type Scraper interface {
	scrape(ch chan<- prometheus.Metric, report *rtreport.LiteSpeedReport)
}
<file_sep>package main

import (
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"github.com/prometheus/common/log"
	"github.com/prometheus/common/version"
	"gopkg.in/alecthomas/kingpin.v2"

	"github.com/myokoo/litespeed_exporter/collector"
	"github.com/myokoo/litespeed_exporter/pkg/rtreport"
)

var (
	listenAddress = kingpin.Flag(
		"web.listen-address",
		"Listen address for web interface and telemetry.",
	).Default(":9104").String()
	metricPath = kingpin.Flag(
		"web.telemetry-path",
		"URL path under which to expose metrics.",
	).Default("/metrics").String()
	reportPath = kingpin.Flag(
		"lsws.report-path",
		"Filesystem path under which exist lsws real-time statistics reports.",
	).Default(rtreport.DefaultReportPath).String()
)

func main() {
	// Parse flags.
	log.AddFlags(kingpin.CommandLine)
	kingpin.Version(version.Print("litespeed_exporter"))
	kingpin.HelpFlag.Short('h')
	kingpin.Parse()

	// landingPage contains the HTML served at '/'.
	// TODO: Make this nicer and more informative.
	var landingPage = []byte(`<html>
<head><title>LiteSpeed exporter</title></head>
<body>
<h1>LiteSpeed exporter</h1>
<p><a href='` + *metricPath + `'>Metrics</a></p>
</body>
</html>
`)

	log.Infoln("Starting litespeed_exporter", version.Info())
	log.Infoln("Build context", version.BuildContext())
	log.Infoln("Listening on", *listenAddress)

	exporter := collector.New(reportPath)
	prometheus.MustRegister(exporter)
	prometheus.MustRegister(version.NewCollector("litespeed_exporter"))

	http.Handle(*metricPath, promhttp.Handler())
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write(landingPage) })
	log.Fatal(http.ListenAndServe(*listenAddress, nil))
}
<file_sep>package collector

import (
	"github.com/prometheus/client_golang/prometheus"

	"github.com/myokoo/litespeed_exporter/pkg/rtreport"
)

var (
	extAppLabels = []string{"type", "vhost", "extapp_name"}
	eName        = "external_application"
)

type extApp struct{}

func (e extApp) scrape(ch chan<- prometheus.Metric, report *rtreport.LiteSpeedReport) {
	for typeName, vhostMap := range report.ExtAppReports {
		for vhost, extAppMap := range vhostMap {
			for extAppName, valueMap := range extAppMap {
				for key, value := range valueMap {
					switch key {
					case rtreport.ExtAppKeyMaxConn:
						ch <- newMetric(
							namespace, eName, "max_connections",
							"The max possible connections value of external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					case rtreport.ExtAppKeyEffectiveMaxConn:
						ch <- newMetric(
							namespace, eName, "effective_max_connections",
							"The max possible effective connections value of external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					case rtreport.ExtAppKeyPoolSize:
						ch <- newMetric(
							namespace, eName, "pool_size",
							"The pool size by external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					case rtreport.ExtAppKeyInUseConn:
						ch <- newMetric(
							namespace, eName, "connection_used",
							"The number of used connections by external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					case rtreport.ExtAppKeyIdleConn:
						ch <- newMetric(
							namespace, eName, "connection_idles",
							"The number of idle connections by external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					case rtreport.ExtAppKeyWaitQueue:
						ch <- newMetric(
							namespace, eName, "wait_queues",
							"The number of wait queues by external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					case rtreport.ExtAppKeyReqPerSec:
						ch <- newMetric(
							namespace, eName, "requests_per_sec",
							"The total requests per second by external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					case rtreport.ExtAppKeyReqTotal:
						ch <- newMetric(
							namespace, eName, "requests_total",
							"The total requests by external application.",
							extAppLabels, prometheus.GaugeValue, value, typeName, vhost, extAppName,
						)
					}
				}
			}
		}
	}
}
<file_sep>package rtreport

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
)

type LineParser interface {
	parse(report *LiteSpeedReport)
}

func NewLineParser(lineTxt string) LineParser {
	var v LineParser
	switch {
	case strings.HasPrefix(lineTxt, "VERSION:"):
		v = versionLine(lineTxt)
	case strings.HasPrefix(lineTxt, "UPTIME:"):
		v = uptimeLine(lineTxt)
	case strings.HasPrefix(lineTxt, "BPS_IN:"):
		v = networkLine(lineTxt)
	case strings.HasPrefix(lineTxt, "MAXCONN:"):
		v = connectionLine(lineTxt)
	case strings.HasPrefix(lineTxt, "REQ_RATE"):
		v = virtualHostLine(lineTxt)
	case strings.HasPrefix(lineTxt, "EXTAPP"):
		v = extAppLine(lineTxt)
	default:
		v = ignoreLine(lineTxt)
	}
	return v
}

type versionLine string

// VERSION: LiteSpeed Web Server/Enterprise/x.x.x to x.x.x
func (v versionLine) parse(report *LiteSpeedReport) {
	lineText := string(v)
	if len(lineText) < 10 {
		report.error = newTooShortParseLineError(lineText)
		return
	}
	s := strings.Split(lineText, "/")
	report.Version = s[len(s)-1]
}

type uptimeLine string

// parse UPTIME: xx:xx:xx
func (u uptimeLine) parse(report *LiteSpeedReport) {
	lineText := string(u)
	if len(lineText) < 16 {
		report.error = newTooShortParseLineError(lineText)
		return
	}
	v := strings.Split(lineText[8:], ":")
	if len(v) != 3 {
		report.error = errors.New(fmt.Sprintf("%s: Expected 3 parts after split, got %d.", lineText[8:], len(v)))
		return
	}
	h, _ := strconv.ParseUint(v[0], 10, 64)
	m, _ := strconv.ParseUint(v[1], 10, 64)
	s, _ := strconv.ParseUint(v[2], 10, 64)
	report.Uptime = float64((h * 60 * 60) + (m * 60) + s)
	return
}

type networkLine string

// parse BPS_IN: x, BPS_OUT: x, SSL_BPS_IN: x, SSL_BPS_OUT: x
func (n networkLine) parse(report *LiteSpeedReport) {
	report.NetworkReport, report.error = convertStringToMap(string(n))
}

type connectionLine string

// parse MAXCONN: 10000, MAXSSL_CONN: 5000, PLAINCONN: 1, AVAILCONN: 9999, IDLECONN: 0, SSLCONN: 0, AVAILSSL: 5000
func (c connectionLine) parse(report *LiteSpeedReport) {
	report.ConnectionReport, report.error = convertStringToMap(string(c))
}

type virtualHostLine string

// parse REQ_RATE [xxxx]: REQ_PROCESSING: 1, REQ_PER_SEC: 0.1, TOT_REQS: 152, PUB_CACHE_HITS_PER_SEC: 0.0, TOTAL_PUB_CACHE_HITS: 0, PRIVATE_CACHE_HITS_PER_SEC: 0.0, TOTAL_PRIVATE_CACHE_HITS: 0, STATIC_HITS_PER_SEC: 0.0, TOTAL_STATIC_HITS: 47
func (r virtualHostLine) parse(report *LiteSpeedReport) {
	lineText := string(r)

	// pick up vhostName
	s := pickUpStringName(lineText)
	if len(s) < 1 {
		report.error = errors.New(fmt.Sprintf("%s: Unable to parse VirtualHostName.", lineText))
		return
	}
	vhName := s[0]
	if vhName == "" {
		vhName = "Server"
	}

	i := strings.Index(lineText, "]:")
	report.VirtualHostReport[vhName], report.error = convertStringToMap(lineText[i+2:])
}

type extAppLine string

// parse EXTAPP [xxxx] [xxxx] [xxxx]: CMAXCONN: 1000, EMAXCONN: 1000, POOL_SIZE: 1, INUSE_CONN: 1, IDLE_CONN: 0, WAITQUE_DEPTH: 0, REQ_PER_SEC: 0.0, TOT_REQS: 0
func (e extAppLine) parse(report *LiteSpeedReport) {
	lineText := string(e)

	// pick up ExtAppType, vhostName, ExtAppName
	s := pickUpStringName(lineText)
	if len(s) < 3 {
		report.error = errors.New(fmt.Sprintf("%s: Unable to parse ExtAppType, VirtualHostName, ExtAppName.", lineText))
		return
	}
	vhostName := s[1]
	if vhostName == "" {
		vhostName = "Server"
	}
	i := strings.Index(lineText, "]:")
	var m map[string]float64
	m, report.error = convertStringToMap(lineText[i+2:])

	if _, exist := report.ExtAppReports[s[0]]; !exist {
		report.ExtAppReports[s[0]] = map[string]map[string]map[string]float64{vhostName: {s[2]: m}}
	} else if _, exit := report.ExtAppReports[s[0]][vhostName]; !exit {
		report.ExtAppReports[s[0]][vhostName] = map[string]map[string]float64{s[2]: m}
	} else {
		report.ExtAppReports[s[0]][vhostName][s[2]] = m
	}
}

type ignoreLine string

// does not parse.
func (i ignoreLine) parse(report *LiteSpeedReport) {
}

// convert "xxxx: 1234, oooo: 4321" strings to map[string]float64{"xxxx":1234, "oooo":4321}
func convertStringToMap(lineText string) (map[string]float64, error) {
	m := make(map[string]float64)
	keyValues := strings.Split(lineText, ",")

	for _, keyValue := range keyValues {
		s := strings.Split(keyValue, ":")
		if len(s) < 2 {
			return nil, errors.New(fmt.Sprintf("%s: Unable to split item to key/value.", keyValue))
		}
		var err error
		if m[strings.TrimSpace(s[0])], err = strconv.ParseFloat(strings.TrimSpace(s[1]), 64); err != nil {
			return nil, errors.New(fmt.Sprintf("%s: Unable to convert string to float64.", strings.TrimSpace(s[1])))
		}
	}
	return m, nil
}

// pick up "[]string{"oooo", "oooo"}" from "XXXX [oooo] [oooo]: xxxxx"
func pickUpStringName(lineText string) []string {
	var s []string
	for i := strings.Count(lineText, "["); i > 0; i-- {
		startIndex := strings.Index(lineText, "[")
		endIndex := strings.Index(lineText, "]")
		s = append(s, strings.TrimSpace(lineText[startIndex+1:endIndex]))
		lineText = lineText[endIndex+1:]
	}
	return s
}

// create parse line too short error.
func newTooShortParseLineError(s string) error {
	return errors.New(fmt.Sprintf("%s: Parsed line too short.", s))
}
<file_sep>package collector

import (
	"github.com/prometheus/client_golang/prometheus"

	"github.com/myokoo/litespeed_exporter/pkg/rtreport"
)

var (
	connectionLabel = []string{"scheme"}
	cName           = "server_connection"
)

type connection struct{}

func (c connection) scrape(ch chan<- prometheus.Metric, report *rtreport.LiteSpeedReport) {
	for key, value := range report.ConnectionReport {
		switch key {
		case rtreport.ConnectionReportKeyMaxConn:
			ch <- newMetric(
				namespace, cName, "max",
				"The maximum http connections value of server.",
				connectionLabel, prometheus.GaugeValue, value, "http",
			)
		case rtreport.ConnectionReportKeyMaxConnSsl:
			ch <- newMetric(
				namespace, cName, "max",
				"The maximum https connections value of server.",
				connectionLabel, prometheus.GaugeValue, value, "https",
			)
		case rtreport.ConnectionReportKeyIdleConn:
			ch <- newMetric(
				namespace, cName, "idle",
				"The current idle connections value of server.",
				nil, prometheus.GaugeValue, value,
			)
		case rtreport.ConnectionReportKeyUsedConn:
			ch <- newMetric(
				namespace, cName, "used",
				"The current number of used http connections to server.",
				connectionLabel, prometheus.GaugeValue, value, "http",
			)
		case rtreport.ConnectionReportKeyUsedConnSsl:
			ch <- newMetric(
				namespace, cName, "used",
				"The current number of used https connections to server.",
				connectionLabel, prometheus.GaugeValue, value, "https",
			)
		}
	}
}
<file_sep>package rtreport

func mergeSingleMap(a, b map[string]float64) {
	for key, value := range b {
		if _, exist := a[key]; !exist {
			a[key] = value
		} else {
			a[key] = a[key] + value
		}
	}
}

func mergeDoubleMap(a, b map[string]map[string]float64) {
	for key, value := range b {
		if _, exist := a[key]; !exist {
			a[key] = value
		} else {
			mergeSingleMap(a[key], b[key])
		}
	}
}

func mergeTripleMap(a, b map[string]map[string]map[string]float64) {
	for key, value := range b {
		if _, exist := a[key]; !exist {
			a[key] = value
		} else {
			mergeDoubleMap(a[key], b[key])
		}
	}
}

func mergeQuadrupleMap(a, b map[string]map[string]map[string]map[string]float64) {
	for key, value := range b {
		if _, exist := a[key]; !exist {
			a[key] = value
		} else {
			mergeTripleMap(a[key], b[key])
		}
	}
}
<file_sep>module github.com/myokoo/litespeed_exporter

go 1.16

require (
	github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
	github.com/golang/protobuf v1.5.2 // indirect
	github.com/google/go-cmp v0.5.5
	github.com/google/go-querystring v1.1.0 // indirect
	github.com/prometheus/client_golang v1.11.0
	github.com/prometheus/common v0.29.0
	github.com/prometheus/promu v0.12.0 // indirect
	go.uber.org/atomic v1.8.0 // indirect
	golang.org/x/net v0.0.0-20210610132358-84b48f89b13b // indirect
	golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
	golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect
	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
	google.golang.org/appengine v1.6.7 // indirect
	gopkg.in/alecthomas/kingpin.v2 v2.2.6
	gopkg.in/yaml.v2 v2.4.0 // indirect
)

replace github.com/prometheus/common v0.29.0 => github.com/prometheus/common v0.26.0
<file_sep>package rtreport

import (
	"reflect"
	"testing"

	"github.com/google/go-cmp/cmp"
)

func TestNewLineParser(t *testing.T) {
	tests := []struct {
		name string
		args string
		want LineParser
	}{
		{
			name: "ok_versionLine",
			args: "VERSION: LiteSpeed Web Server/Enterprise/5.8",
			want: versionLine("VERSION: LiteSpeed Web Server/Enterprise/5.8"),
		},
		{
			name: "ok_uptimeLine",
			args: "UPTIME: 03:02:01",
			want: uptimeLine("UPTIME: 03:02:01"),
		},
		{
			name: "ok_networkLine",
			args: "BPS_IN: 1, BPS_OUT: 2, SSL_BPS_IN: 3, SSL_BPS_OUT: 4",
			want: networkLine("BPS_IN: 1, BPS_OUT: 2, SSL_BPS_IN: 3, SSL_BPS_OUT: 4"),
		},
		{
			name: "ok_connectionLine",
			args: "MAXCONN: 10000, MAXSSL_CONN: 5000, PLAINCONN: 1, AVAILCONN: 9999, IDLECONN: 0, SSLCONN: 0, AVAILSSL: 5000",
			want: connectionLine("MAXCONN: 10000, MAXSSL_CONN: 5000, PLAINCONN: 1, AVAILCONN: 9999, IDLECONN: 0, SSLCONN: 0, AVAILSSL: 5000"),
		},
		{
			name: "ok_requestLine",
			args: "REQ_RATE [hoge.com]: REQ_PROCESSING: 1, REQ_PER_SEC: 0.1, TOT_REQS: 152, PUB_CACHE_HITS_PER_SEC: 0.0, TOTAL_PUB_CACHE_HITS: 0, " +
				"PRIVATE_CACHE_HITS_PER_SEC: 0.0, TOTAL_PRIVATE_CACHE_HITS: 0, STATIC_HITS_PER_SEC: 0.0, TOTAL_STATIC_HITS: 47",
			want: virtualHostLine("REQ_RATE [hoge.com]: REQ_PROCESSING: 1, REQ_PER_SEC: 0.1, TOT_REQS: 152, PUB_CACHE_HITS_PER_SEC: 0.0, " +
				"TOTAL_PUB_CACHE_HITS: 0, PRIVATE_CACHE_HITS_PER_SEC: 0.0, TOTAL_PRIVATE_CACHE_HITS: 0, STATIC_HITS_PER_SEC: 0.0, TOTAL_STATIC_HITS: 47"),
		},
		{
			name: "ok_extAppLine",
			args: "EXTAPP [LSAPI] [hoge.com] [hoge.com_php7.3]: CMAXCONN: 1000, EMAXCONN: 1000, POOL_SIZE: 1, " +
				"INUSE_CONN: 1, IDLE_CONN: 0, WAITQUE_DEPTH: 0, REQ_PER_SEC: 0.0, TOT_REQS: 0",
			want: extAppLine("EXTAPP [LSAPI] [hoge.com] [hoge.com_php7.3]: CMAXCONN: 1000, EMAXCONN: 1000, POOL_SIZE: 1, " +
				"INUSE_CONN: 1, IDLE_CONN: 0, WAITQUE_DEPTH: 0, REQ_PER_SEC: 0.0, TOT_REQS: 0"),
		},
		{
			name: "ok_ignoreLine",
			args: "BLOCKED_IP:",
			want: ignoreLine("BLOCKED_IP:"),
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := NewLineParser(tt.args); cmp.Equal(got, tt.want) != true {
				t.Errorf("NewLineParser() = %v, want %v, type: %v, %v", got, tt.want, reflect.TypeOf(got), reflect.TypeOf(tt.want))
			}
		})
	}
}

func Test_versionLine_parse(t *testing.T) {
	tests := []struct {
		name    string
		v       versionLine
		args    LiteSpeedReport
		want    string
		wantErr bool
	}{
		{
			name:    "ok",
			v:       versionLine("VERSION: LiteSpeed Web Server/Enterprise/5.8.1"),
			args:    LiteSpeedReport{},
			want:    "5.8.1",
			wantErr: false,
		},
		{
			name:    "ng",
			v:       versionLine("5.8"),
			args:    LiteSpeedReport{},
			want:    "",
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.v.parse(&tt.args)
			if (tt.args.error != nil) != tt.wantErr {
				t.Errorf("(versionLine)parse() error = %v, wantErr %v", tt.args.error, tt.wantErr)
			}
			if !tt.wantErr && tt.args.Version != tt.want {
				t.Errorf("(versionLine)parse() does not match. got = %v, want = %v", tt.args.Version, tt.want)
			}
		})
	}
}

func Test_uptimeLine_parse(t *testing.T) {
	tests := []struct {
		name    string
		u       uptimeLine
		args    LiteSpeedReport
		want    float64
		wantErr bool
	}{
		{
			name:    "ok",
			u:       uptimeLine("UPTIME: 03:02:01"),
			args:    LiteSpeedReport{},
			want:    10921,
			wantErr: false,
		},
		{
			name:    "ng",
			u:       uptimeLine("03:02:01"),
			args:    LiteSpeedReport{},
			want:    0,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.u.parse(&tt.args)
			if (tt.args.error != nil) != tt.wantErr {
				t.Errorf("(uptimeLine)parse() error = %v, wantErr %v", tt.args.error, tt.wantErr)
			}
			if !tt.wantErr && tt.args.Uptime != tt.want {
				t.Errorf("(uptimeLine)parse() does not match. got = %v, want = %v", tt.args.Uptime, tt.want)
			}
		})
	}
}

func Test_networkLine_parse(t *testing.T) {
	tests := []struct {
		name    string
		n       networkLine
		args    LiteSpeedReport
		want    LiteSpeedReport
		wantErr bool
	}{
		{
			name: "ok",
			n:    networkLine("BPS_IN: 2, BPS_OUT: 1954, SSL_BPS_IN: 5, SSL_BPS_OUT: 3332"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				NetworkReport: map[string]float64{
					"BPS_IN":      2,
					"BPS_OUT":     1954,
					"SSL_BPS_IN":  5,
					"SSL_BPS_OUT": 3332,
				},
			},
			wantErr: false,
		},
		{
			name:    "ng",
			n:       networkLine("BPS_IN: 2 BPS_OUT: 1954"),
			args:    LiteSpeedReport{},
			want:    LiteSpeedReport{},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.n.parse(&tt.args)
			if (tt.args.error != nil) != tt.wantErr {
				t.Errorf("(networkLine)parse() error = %v, wantErr %v", tt.args.error, tt.wantErr)
			}
			if !tt.wantErr && !cmp.Equal(tt.args.NetworkReport, tt.want.NetworkReport) {
				t.Errorf("(networkLine)parse() does not match. got = %v, want = %v", tt.args.NetworkReport, tt.want.NetworkReport)
			}
		})
	}
}

func Test_connectionLine_parse(t *testing.T) {
	tests := []struct {
		name    string
		c       connectionLine
		args    LiteSpeedReport
		want    LiteSpeedReport
		wantErr bool
	}{
		{
			name: "ok",
			c:    connectionLine("MAXCONN: 10000, MAXSSL_CONN: 5000, PLAINCONN: 100, AVAILCONN: 200, IDLECONN: 1, SSLCONN: 2, AVAILSSL: 3"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				ConnectionReport: map[string]float64{
					"MAXCONN":     10000,
					"MAXSSL_CONN": 5000,
					"PLAINCONN":   100,
					"AVAILCONN":   200,
					"IDLECONN":    1,
					"SSLCONN":     2,
					"AVAILSSL":    3,
				},
			},
			wantErr: false,
		},
		{
			name:    "ng",
			c:       connectionLine("MAXCONN: 10000 MAXSSL_CONN: 5000, PLAINCONN: 100, AVAILCONN: 200, IDLECONN: 1, SSLCONN: 2"),
			args:    LiteSpeedReport{},
			want:    LiteSpeedReport{},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.c.parse(&tt.args)
			if (tt.args.error != nil) != tt.wantErr {
				t.Errorf("(connectionLine)parse() error = %v, wantErr %v", tt.args.error, tt.wantErr)
			}
			if !tt.wantErr && !cmp.Equal(tt.args.ConnectionReport, tt.want.ConnectionReport) {
				t.Errorf("(connectionLine)parse() does not match. got = %v, want = %v", tt.args.ConnectionReport, tt.want.ConnectionReport)
			}
		})
	}
}

func Test_ignoreLine_parse(t *testing.T) {
	tests := []struct {
		name    string
		i       ignoreLine
		args    LiteSpeedReport
		want    LiteSpeedReport
		wantErr bool
	}{
		{
			name:    "ok. not store",
			i:       "BLOCKED_IP:",
			args:    LiteSpeedReport{},
			want:    LiteSpeedReport{},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.i.parse(&tt.args)
			if (tt.args.error != nil) != tt.wantErr {
				t.Errorf("(ignoreLine)parse() error = %v, wantErr %v", tt.args.error, tt.wantErr)
			}
			if !tt.wantErr && !cmp.Equal(tt.args.ConnectionReport, tt.want.ConnectionReport) {
				t.Errorf("(ignoreLine)parse() does not match. got = %v, want = %v", tt.args, tt.want)
			}
		})
	}
}

func Test_requestLine_parse(t *testing.T) {
	tests := []struct {
		name    string
		r       virtualHostLine
		args    LiteSpeedReport
		want    LiteSpeedReport
		wantErr bool
	}{
		{
			name: "ok_vhost",
			r: virtualHostLine("REQ_RATE [hoge.jp]: REQ_PROCESSING: 1, REQ_PER_SEC: 0.1, TOT_REQS: 2, PUB_CACHE_HITS_PER_SEC: 0.2, TOTAL_PUB_CACHE_HITS: 3, " +
				"PRIVATE_CACHE_HITS_PER_SEC: 0.3, TOTAL_PRIVATE_CACHE_HITS: 4, STATIC_HITS_PER_SEC: 0.4, TOTAL_STATIC_HITS: 5"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				VirtualHostReport: map[string]map[string]float64{
					"hoge.jp": {
						"REQ_PROCESSING":             1,
						"REQ_PER_SEC":                0.1,
						"TOT_REQS":                   2,
						"PUB_CACHE_HITS_PER_SEC":     0.2,
						"TOTAL_PUB_CACHE_HITS":       3,
						"PRIVATE_CACHE_HITS_PER_SEC": 0.3,
						"TOTAL_PRIVATE_CACHE_HITS":   4,
						"STATIC_HITS_PER_SEC":        0.4,
						"TOTAL_STATIC_HITS":          5,
					},
				},
			},
			wantErr: false,
		},
		{
			name: "ok_vhost_and_port",
			r: virtualHostLine("REQ_RATE [hoge.jp:80]: REQ_PROCESSING: 1, REQ_PER_SEC: 0.1, TOT_REQS: 2, PUB_CACHE_HITS_PER_SEC: 0.2, TOTAL_PUB_CACHE_HITS: 3, " +
				"PRIVATE_CACHE_HITS_PER_SEC: 0.3, TOTAL_PRIVATE_CACHE_HITS: 4, STATIC_HITS_PER_SEC: 0.4, TOTAL_STATIC_HITS: 5"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				VirtualHostReport: map[string]map[string]float64{
					"hoge.jp:80": {
						"REQ_PROCESSING":             1,
						"REQ_PER_SEC":                0.1,
						"TOT_REQS":                   2,
						"PUB_CACHE_HITS_PER_SEC":     0.2,
						"TOTAL_PUB_CACHE_HITS":       3,
						"PRIVATE_CACHE_HITS_PER_SEC": 0.3,
						"TOTAL_PRIVATE_CACHE_HITS":   4,
						"STATIC_HITS_PER_SEC":        0.4,
						"TOTAL_STATIC_HITS":          5,
					},
				},
			},
			wantErr: false,
		},
		{
			name: "ok_Server",
			r: virtualHostLine("REQ_RATE []: REQ_PROCESSING: 2, REQ_PER_SEC: 0.3, TOT_REQS: 5, PUB_CACHE_HITS_PER_SEC: 0.6, TOTAL_PUB_CACHE_HITS: 3, " +
				"PRIVATE_CACHE_HITS_PER_SEC: 0.3, TOTAL_PRIVATE_CACHE_HITS: 4, STATIC_HITS_PER_SEC: 0.4, TOTAL_STATIC_HITS: 5"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				VirtualHostReport: map[string]map[string]float64{
					"Server": {
						"REQ_PROCESSING":             2,
						"REQ_PER_SEC":                0.3,
						"TOT_REQS":                   5,
						"PUB_CACHE_HITS_PER_SEC":     0.6,
						"TOTAL_PUB_CACHE_HITS":       3,
						"PRIVATE_CACHE_HITS_PER_SEC": 0.3,
						"TOTAL_PRIVATE_CACHE_HITS":   4,
						"STATIC_HITS_PER_SEC":        0.4,
						"TOTAL_STATIC_HITS":          5,
					},
				},
			},
			wantErr: false,
		},
		{
			name:    "ng",
			r:       virtualHostLine("REQ_RATE [hoge.jp]:: REQ_PROCESSING: 1, REQ_PER_SEC: 0.1, TOT_REQS: 2, PUB_CACHE_HITS_PER_SEC: 0.2, TOTAL_PUB_CACHE_HITS: 3"),
			args:    LiteSpeedReport{},
			want:    LiteSpeedReport{},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.args.VirtualHostReport = make(map[string]map[string]float64)
			tt.r.parse(&tt.args)
			if (tt.args.error != nil) != tt.wantErr {
				t.Errorf("(virtualHostLine)parse() error = %v, wantErr %v", tt.args.error, tt.wantErr)
			}
			if !tt.wantErr && !cmp.Equal(tt.args.VirtualHostReport, tt.want.VirtualHostReport) {
				t.Errorf("(virtualHostLine)parse() does not match. got = %v, want = %v", tt.args.VirtualHostReport, tt.want.VirtualHostReport)
			}
		})
	}
}

func Test_extAppLine_parse(t *testing.T) {
	tests := []struct {
		name    string
		e       extAppLine
		args    LiteSpeedReport
		want    LiteSpeedReport
		wantErr bool
	}{
		{
			name: "ok_vhost",
			e:    extAppLine("EXTAPP [LSAPI] [fuga.com] [fuga.com_php73]: CMAXCONN: 1, EMAXCONN: 2, POOL_SIZE: 3, INUSE_CONN: 4, IDLE_CONN: 5, WAITQUE_DEPTH: 6, REQ_PER_SEC: 0.7, TOT_REQS: 8"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				ExtAppReports: map[string]map[string]map[string]map[string]float64{
					"LSAPI": {
						"fuga.com": {
							"fuga.com_php73": {
								"CMAXCONN":      1,
								"EMAXCONN":      2,
								"POOL_SIZE":     3,
								"INUSE_CONN":    4,
								"IDLE_CONN":     5,
								"WAITQUE_DEPTH": 6,
								"REQ_PER_SEC":   0.7,
								"TOT_REQS":      8,
							},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "ok_vhost_port",
			e:    extAppLine("EXTAPP [LSAPI] [fuga.com:80] [fuga.com_php73]: CMAXCONN: 1, EMAXCONN: 2, POOL_SIZE: 3, INUSE_CONN: 4, IDLE_CONN: 5, WAITQUE_DEPTH: 6, REQ_PER_SEC: 0.7, TOT_REQS: 8"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				ExtAppReports: map[string]map[string]map[string]map[string]float64{
					"LSAPI": {
						"fuga.com:80": {
							"fuga.com_php73": {
								"CMAXCONN":      1,
								"EMAXCONN":      2,
								"POOL_SIZE":     3,
								"INUSE_CONN":    4,
								"IDLE_CONN":     5,
								"WAITQUE_DEPTH": 6,
								"REQ_PER_SEC":   0.7,
								"TOT_REQS":      8,
							},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name: "ok_cgi",
			e:    extAppLine("EXTAPP [CGI] [] [lscgid]: CMAXCONN: 2, EMAXCONN: 3, POOL_SIZE: 4, INUSE_CONN: 5, IDLE_CONN: 6, WAITQUE_DEPTH: 7, REQ_PER_SEC: 0.8, TOT_REQS: 9"),
			args: LiteSpeedReport{},
			want: LiteSpeedReport{
				ExtAppReports: map[string]map[string]map[string]map[string]float64{
					"CGI": {
						"Server": {
							"lscgid": {
								"CMAXCONN":      2,
								"EMAXCONN":      3,
								"POOL_SIZE":     4,
								"INUSE_CONN":    5,
								"IDLE_CONN":     6,
								"WAITQUE_DEPTH": 7,
								"REQ_PER_SEC":   0.8,
								"TOT_REQS":      9,
							},
						},
					},
				},
			},
			wantErr: false,
		},
		{
			name:    "ng",
			e:       extAppLine("EXTAPP [LSA:PI] [fuga.com]: CMAXCONN: 2, EMAXCONN: 3, POOL_SIZE: 4, INUSE_CONN: 5, IDLE_CONN: 6, WAITQUE_DEPTH: 7, REQ_PER_SEC: 0.8, TOT_REQS: 9"),
			args:    LiteSpeedReport{},
			want:    LiteSpeedReport{},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			tt.args.ExtAppReports = make(map[string]map[string]map[string]map[string]float64)
			tt.e.parse(&tt.args)
			if (tt.args.error != nil) != tt.wantErr {
				t.Errorf("(extAppLine)parse() error = %v, wantErr %v", tt.args.error, tt.wantErr)
			}
			if !tt.wantErr && !cmp.Equal(tt.args.ExtAppReports, tt.want.ExtAppReports) {
				t.Errorf("(extAppLine)parse() does not match. got = %v, want = %v", tt.args.ExtAppReports, tt.want.ExtAppReports)
			}
		})
	}
}

func Test_pickUpStringName(t *testing.T) {
	tests := []struct {
		name string
		args string
		want []string
	}{
		{
			name: "ok_single",
			args: "REQ_RATE [hoge.com]: REQ_PROCESSING: 1",
			want: []string{"hoge.com"},
		},
		{
			name: "ok_multi",
			args: "EXTAPP [LSAPI] [hoge.com] [hoge.com_php7.3]: CMAXCONN: 1000,",
			want: []string{"LSAPI", "hoge.com", "hoge.com_php7.3"},
		},
		{
			name: "ok_vhost_and_port",
			args: "EXTAPP [LSAPI] [hoge.com:80] [hoge.com_php7.3]: CMAXCONN: 1000,",
			want: []string{"LSAPI", "hoge.com:80", "hoge.com_php7.3"},
		},
		{
			name: "ok_trim_space",
			args: "EXTAPP [LSAPI] [ hoge.com] [hoge.com_php7.3]: CMAXCONN: 1000,",
			want: []string{"LSAPI", "hoge.com", "hoge.com_php7.3"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := pickUpStringName(tt.args); !cmp.Equal(got, tt.want) {
				t.Errorf("pickUpStringName() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_convertStringToMap(t *testing.T) {
	tests := []struct {
		name    string
		args    string
		want    map[string]float64
		wantErr bool
	}{
		{
			name:    "ok_single",
			args:    "xxxx: 1234",
			want:    map[string]float64{"xxxx": 1234},
			wantErr: false,
		},
		{
			name:    "ok_multi",
			args:    "xxxx: 1234, oooo: 432.1",
			want:    map[string]float64{"xxxx": 1234, "oooo": 432.1},
			wantErr: false,
		},
		{
			name:    "ng",
			args:    "xxxx: 1234 oooo: 432.1",
			want:    map[string]float64{},
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := convertStringToMap(tt.args)
			if (err != nil) != tt.wantErr {
				t.Errorf("convertStringToMap() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !tt.wantErr && !cmp.Equal(got, tt.want) {
				t.Errorf("convertStringToMap() got = %v, want %v", got, tt.want)
			}
		})
	}
}
<file_sep>package rtreport

import (
	"testing"

	"github.com/google/go-cmp/cmp"
)

func Test_mergeSingleMap(t *testing.T) {
	type args struct {
		a map[string]float64
		b map[string]float64
	}
	tests := []struct {
		name string
		args args
		want map[string]float64
	}{
		{
			name: "ok",
			args: args{
				a: map[string]float64{"hoge": 10, "fuga": 0.1},
				b: map[string]float64{"hoge": 3, "aaa": 123},
			},
			want: map[string]float64{"hoge": 13, "fuga": 0.1, "aaa": 123},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mergeSingleMap(tt.args.a, tt.args.b)
			if !cmp.Equal(tt.args.a, tt.want) {
				t.Errorf("mergeSingleMap() does not match. got = %v, want = %v", tt.args.a, tt.want)
			}
		})
	}
}

func Test_mergeDoubleMap(t *testing.T) {
	type args struct {
		a map[string]map[string]float64
		b map[string]map[string]float64
	}
	tests := []struct {
		name string
		args args
		want map[string]map[string]float64
	}{
		{
			name: "ok",
			args: args{
				a: map[string]map[string]float64{
					"hoge": {"fuga": 100, "xxxx": 123},
					"aaaa": {"abc": 1.1},
				},
				b: map[string]map[string]float64{
					"hoge": {"fuga": 321, "cccc": 10},
					"bbb":  {"dddd": 3456},
				},
			},
			want: map[string]map[string]float64{
				"hoge": {"fuga": 421, "xxxx": 123, "cccc": 10},
				"aaaa": {"abc": 1.1},
				"bbb":  {"dddd": 3456},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mergeDoubleMap(tt.args.a, tt.args.b)
			if !cmp.Equal(tt.args.a, tt.want) {
				t.Errorf("mergeDoubleMap() does not match. got = %v, want = %v", tt.args.a, tt.want)
			}
		})
	}
}

func Test_mergeTripleMap(t *testing.T) {
	type args struct {
		a map[string]map[string]map[string]float64
		b map[string]map[string]map[string]float64
	}
	tests := []struct {
		name string
		args args
		want map[string]map[string]map[string]float64
	}{
		{

			name: "ok",
			args: args{
				a: map[string]map[string]map[string]float64{"aaa": {"bb": {"a": 1000, "b": 202}}},
				b: map[string]map[string]map[string]float64{"aaa": {"bb": {"a": 21}}, "ccc": {"dd": {"e": 100}}},
			},
			want: map[string]map[string]map[string]float64{"aaa": {"bb": {"a": 1021, "b": 202}}, "ccc": {"dd": {"e": 100}}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mergeTripleMap(tt.args.a, tt.args.b)
			if !cmp.Equal(tt.args.a, tt.want) {
				t.Errorf("mergeTripleMap() does not match. got = %v, want = %v", tt.args.a, tt.want)
			}
		})
	}
}

func Test_mergeQuadrupleMap(t *testing.T) {
	type args struct {
		a map[string]map[string]map[string]map[string]float64
		b map[string]map[string]map[string]map[string]float64
	}
	tests := []struct {
		name string
		args args
		want map[string]map[string]map[string]map[string]float64
	}{
		{

			name: "ok",
			args: args{
				a: map[string]map[string]map[string]map[string]float64{"aaaa": {"bbb": {"aa": {"a": 1000, "h": 544}, "bb": {"a": 1345}}}},
				b: map[string]map[string]map[string]map[string]float64{"aaaa": {"bbb": {"aa": {"h": 21}}}, "cccc": {"ddd": {"ee": {"z:": 456}}}},
			},
			want: map[string]map[string]map[string]map[string]float64{"aaaa": {"bbb": {"aa": {"a": 1000, "h": 565}, "bb": {"a": 1345}}}, "cccc": {"ddd": {"ee": {"z:": 456}}}},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mergeQuadrupleMap(tt.args.a, tt.args.b)
			if !cmp.Equal(tt.args.a, tt.want) {
				t.Errorf("mergeTripleMap() does not match. got = %v, want = %v", tt.args.a, tt.want)
			}
		})
	}
}
<file_sep># litespeed_exporter
Prometheus exporter for LiteSpeed server metrics.

## Building and running
### Compatibility
- Go 1.14+

### Build

```bash
make build
or
make cross_build
```

## usage

```bash
usage: litespeed_exporter [<flags>]

Flags:
  -h, --help              Show context-sensitive help (also try --help-long and --help-man).
      --web.listen-address=":9104"
                          Listen address for web interface and telemetry.
      --web.telemetry-path="/metrics"
                          URL path under which to expose metrics.
      --lsws.report-path="/tmp/lshttpd"
                          Filesystem path under which exist lsws real-time statistics reports.
      --log.level="info"  Only log messages with the given severity or above. Valid levels: [debug, info, warn, error, fatal]
      --log.format="logger:stderr"
                          Set the log target and format. Example: "logger:syslog?appname=bob&local=7" or "logger:stdout?json=true"
      --version           Show application version.

```

## author
@myokoo

<file_sep>package rtreport

import (
	"reflect"
	"testing"

	"github.com/google/go-cmp/cmp"
)

func Test_sum(t *testing.T) {
	type args struct {
		a *LiteSpeedReport
		b *LiteSpeedReport
	}
	tests := []struct {
		name string
		args args
		want *LiteSpeedReport
	}{
		{
			name: "ok",
			args: args{
				a: &LiteSpeedReport{
					Uptime:           123,
					Version:          "5.4",
					NetworkReport:    map[string]float64{"BPS_IN": 123, "BPS_OUT": 713819, "SSL_BPS_IN": 136, "SSL_BPS_OUT": 891290},
					ConnectionReport: map[string]float64{"MAXCONN": 10000, "MAXSSL_CONN": 5000, "PLAINCONN": 2331, "AVAILCONN": 7669, "IDLECONN": 0, "SSLCONN": 5, "AVAILSSL": 4995},
					VirtualHostReport: map[string]map[string]float64{
						"Server": {"REQ_PROCESSING": 3, "REQ_PER_SEC": 3.5, "TOT_REQS": 1533, "PUB_CACHE_HITS_PER_SEC": 0.0, "TOTAL_PUB_CACHE_HITS": 0, "PRIVATE_CACHE_HITS_PER_SEC": 1.1,
							"TOTAL_PRIVATE_CACHE_HITS": 123, "STATIC_HITS_PER_SEC": 4.4, "TOTAL_STATIC_HITS": 49},
						"hoge.com": {"REQ_PROCESSING": 1, "REQ_PER_SEC": 1.5, "TOT_REQS": 133, "PUB_CACHE_HITS_PER_SEC": 2.1,
							"TOTAL_PUB_CACHE_HITS": 345, "PRIVATE_CACHE_HITS_PER_SEC": 4.3, "TOTAL_PRIVATE_CACHE_HITS": 345, "STATIC_HITS_PER_SEC": 5.5, "TOTAL_STATIC_HITS": 813},
					},
					// EXTAPP [xxxx] [xxxx] [xxxx]: CMAXCONN: 1000, EMAXCONN: 1000, POOL_SIZE: 1, INUSE_CONN: 1, IDLE_CONN: 0, WAITQUE_DEPTH: 0, REQ_PER_SEC: 0.0, TOT_REQS: 0
					ExtAppReports: map[string]map[string]map[string]map[string]float64{
						"LSAPI": {"hoge.com": {"hoge.com_php7.3": {"CMAXCONN": 1000, "EMAXCONN": 1000, "POOL_SIZE": 1,
							"INUSE_CONN": 1, "IDLE_CONN": 0, "WAITQUE_DEPTH": 0, "REQ_PER_SEC": 0.0, "TOT_REQS": 0}}},
					},
				},
				b: &LiteSpeedReport{
					Uptime:           123,
					Version:          "5.4",
					NetworkReport:    map[string]float64{"BPS_IN": 21213, "BPS_OUT": 343819, "SSL_BPS_IN": 123363, "SSL_BPS_OUT": 913290},
					ConnectionReport: map[string]float64{"MAXCONN": 10000, "MAXSSL_CONN": 5000, "PLAINCONN": 1000, "AVAILCONN": 9000, "IDLECONN": 1, "SSLCONN": 100, "AVAILSSL": 4900},
					VirtualHostReport: map[string]map[string]float64{
						"Server": {"REQ_PROCESSING": 5, "REQ_PER_SEC": 6.6, "TOT_REQS": 903, "PUB_CACHE_HITS_PER_SEC": 3.8, "TOTAL_PUB_CACHE_HITS": 1100, "PRIVATE_CACHE_HITS_PER_SEC": 5.3,
							"TOTAL_PRIVATE_CACHE_HITS": 9393, "STATIC_HITS_PER_SEC": 7.9, "TOTAL_STATIC_HITS": 3939},
					},
					ExtAppReports: map[string]map[string]map[string]map[string]float64{
						"LSAPI": {"hoge.com": {"hoge.com_php7.3": {"CMAXCONN": 1000, "EMAXCONN": 1000, "POOL_SIZE": 2,
							"INUSE_CONN": 4, "IDLE_CONN": 3, "WAITQUE_DEPTH": 2, "REQ_PER_SEC": 1.2, "TOT_REQS": 3}}},
						"CGI": {"Server": {"lscgid": {"CMAXCONN": 1000, "EMAXCONN": 1000, "POOL_SIZE": 1,
							"INUSE_CONN": 1, "IDLE_CONN": 0, "WAITQUE_DEPTH": 0, "REQ_PER_SEC": 0.0, "TOT_REQS": 0}}},
					},
				},
			},
			want: &LiteSpeedReport{
				Uptime:           123,
				Version:          "5.4",
				NetworkReport:    map[string]float64{"BPS_IN": 21336, "BPS_OUT": 1057638, "SSL_BPS_IN": 123499, "SSL_BPS_OUT": 1804580},
				ConnectionReport: map[string]float64{"MAXCONN": 20000, "MAXSSL_CONN": 10000, "PLAINCONN": 3331, "AVAILCONN": 16669, "IDLECONN": 1, "SSLCONN": 105, "AVAILSSL": 9895},
				VirtualHostReport: map[string]map[string]float64{
					"Server": {"REQ_PROCESSING": 8, "REQ_PER_SEC": 10.1, "TOT_REQS": 2436, "PUB_CACHE_HITS_PER_SEC": 3.8, "TOTAL_PUB_CACHE_HITS": 1100, "PRIVATE_CACHE_HITS_PER_SEC": 6.4,
						"TOTAL_PRIVATE_CACHE_HITS": 9516, "STATIC_HITS_PER_SEC": 12.3, "TOTAL_STATIC_HITS": 3988},
					"hoge.com": {"REQ_PROCESSING": 1, "REQ_PER_SEC": 1.5, "TOT_REQS": 133, "PUB_CACHE_HITS_PER_SEC": 2.1,
						"TOTAL_PUB_CACHE_HITS": 345, "PRIVATE_CACHE_HITS_PER_SEC": 4.3, "TOTAL_PRIVATE_CACHE_HITS": 345, "STATIC_HITS_PER_SEC": 5.5, "TOTAL_STATIC_HITS": 813},
				},
				ExtAppReports: map[string]map[string]map[string]map[string]float64{
					"LSAPI": {"hoge.com": {"hoge.com_php7.3": {"CMAXCONN": 2000, "EMAXCONN": 2000, "POOL_SIZE": 3,
						"INUSE_CONN": 5, "IDLE_CONN": 3, "WAITQUE_DEPTH": 2, "REQ_PER_SEC": 1.2, "TOT_REQS": 3}}},
					"CGI": {"Server": {"lscgid": {"CMAXCONN": 1000, "EMAXCONN": 1000, "POOL_SIZE": 1,
						"INUSE_CONN": 1, "IDLE_CONN": 0, "WAITQUE_DEPTH": 0, "REQ_PER_SEC": 0.0, "TOT_REQS": 0}}},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := sum(tt.args.a, tt.args.b)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("sum() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_load(t *testing.T) {
	tests := []struct {
		name string
		args string
		want *LiteSpeedReport
	}{
		{
			name: "ok",
			args: "../test/data/load/.rtreport",
			want: &LiteSpeedReport{
				Version:          "5.4",
				Uptime:           56070,
				NetworkReport:    map[string]float64{"BPS_IN": 1, "BPS_OUT": 2, "SSL_BPS_IN": 3, "SSL_BPS_OUT": 4},
				ConnectionReport: map[string]float64{"MAXCONN": 10000, "MAXSSL_CONN": 5000, "PLAINCONN": 0, "AVAILCONN": 10000, "IDLECONN": 0, "SSLCONN": 0, "AVAILSSL": 5000},
				VirtualHostReport: map[string]map[string]float64{
					"Server": {"REQ_PROCESSING": 0, "REQ_PER_SEC": 0.1, "TOT_REQS": 448, "PUB_CACHE_HITS_PER_SEC": 0.0, "TOTAL_PUB_CACHE_HITS": 0, "PRIVATE_CACHE_HITS_PER_SEC": 0.0,
						"TOTAL_PRIVATE_CACHE_HITS": 0, "STATIC_HITS_PER_SEC": 0.1, "TOTAL_STATIC_HITS": 133},
					"hoge.jp": {"REQ_PROCESSING": 3, "REQ_PER_SEC": 2.1, "TOT_REQS": 121, "PUB_CACHE_HITS_PER_SEC": 4.0,
						"TOTAL_PUB_CACHE_HITS": 345, "PRIVATE_CACHE_HITS_PER_SEC": 4.3, "TOTAL_PRIVATE_CACHE_HITS": 345, "STATIC_HITS_PER_SEC": 5.5, "TOTAL_STATIC_HITS": 813},
				},
				ExtAppReports: make(map[string]map[string]map[string]map[string]float64),
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := load(tt.args)
			if !cmp.Equal(got, tt.want, cmp.AllowUnexported(LiteSpeedReport{})) {
				t.Errorf("load() = %v, want %v", *got, *tt.want)
			}
		})
	}
}

func TestNew(t *testing.T) {
	tests := []struct {
		name    string
		args    string
		want    *LiteSpeedReport
		wantErr bool
	}{
		{
			name: "ok",
			args: "../test/data/new",
			want: &LiteSpeedReport{
				Version:          "5.4",
				Uptime:           56070,
				NetworkReport:    map[string]float64{"BPS_IN": 2, "BPS_OUT": 4, "SSL_BPS_IN": 6, "SSL_BPS_OUT": 8},
				ConnectionReport: map[string]float64{"MAXCONN": 20000, "MAXSSL_CONN": 10000, "PLAINCONN": 0, "AVAILCONN": 20000, "IDLECONN": 0, "SSLCONN": 0, "AVAILSSL": 10000},
				VirtualHostReport: map[string]map[string]float64{
					"Server": {"REQ_PROCESSING": 0, "REQ_PER_SEC": 0.2, "TOT_REQS": 896, "PUB_CACHE_HITS_PER_SEC": 0.0, "TOTAL_PUB_CACHE_HITS": 0, "PRIVATE_CACHE_HITS_PER_SEC": 0.0,
						"TOTAL_PRIVATE_CACHE_HITS": 0, "STATIC_HITS_PER_SEC": 0.2, "TOTAL_STATIC_HITS": 266},
					"hoge.jp": {"REQ_PROCESSING": 6, "REQ_PER_SEC": 4.2, "TOT_REQS": 242, "PUB_CACHE_HITS_PER_SEC": 8.0,
						"TOTAL_PUB_CACHE_HITS": 690, "PRIVATE_CACHE_HITS_PER_SEC": 8.6, "TOTAL_PRIVATE_CACHE_HITS": 690, "STATIC_HITS_PER_SEC": 11.0, "TOTAL_STATIC_HITS": 1626},
				},
				ExtAppReports: map[string]map[string]map[string]map[string]float64{
					"LSAPI": {"hoge.jp": {"hoge.jp_php73": {"CMAXCONN": 1000, "EMAXCONN": 1000, "POOL_SIZE": 1,
						"INUSE_CONN": 1, "IDLE_CONN": 0, "WAITQUE_DEPTH": 0, "REQ_PER_SEC": 0.0, "TOT_REQS": 0}}},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := New(tt.args)
			if (err != nil) != tt.wantErr {
				t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !cmp.Equal(got, tt.want, cmp.AllowUnexported(LiteSpeedReport{})) {
				t.Errorf("New() got = %v, want %v", got, tt.want)
			}
		})
	}
}
<file_sep>package rtreport

import (
	"bufio"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
)

// Constants
const (
	DefaultReportPath    = "/tmp/lshttpd"
	reportFileNamePrefix = ".rtreport"
)

// MapKey
const (
	NetworkReportKeyBpsIn          = "BPS_IN"
	NetworkReportKeyBpsOut         = "BPS_OUT"
	NetworkReportKeySslBpsIn       = "SSL_BPS_IN"
	NetworkReportKeySslBpsOut      = "SSL_BPS_OUT"
	ConnectionReportKeyMaxConn     = "MAXCONN"
	ConnectionReportKeyMaxConnSsl  = "MAXSSL_CONN"
	ConnectionReportKeyUsedConn    = "PLAINCONN"
	ConnectionReportKeyIdleConn    = "IDLECONN"
	ConnectionReportKeyUsedConnSsl = "SSLCONN"
	VHostReportKeyProcessing       = "REQ_PROCESSING"
	VhostReportKeyReqPerSec        = "REQ_PER_SEC"
	VHostReportKeyReqTotal         = "TOT_REQS"
	VHostReportKeyPubCacheHits     = "TOTAL_PUB_CACHE_HITS"
	VHostReportKeyPteCacheHits     = "TOTAL_PRIVATE_CACHE_HITS"
	VHostReportKeyStaticHits       = "TOTAL_STATIC_HITS"
	ExtAppKeyMaxConn               = "CMAXCONN"
	ExtAppKeyEffectiveMaxConn      = "EMAXCONN"
	ExtAppKeyPoolSize              = "POOL_SIZE"
	ExtAppKeyInUseConn             = "INUSE_CONN"
	ExtAppKeyIdleConn              = "IDLE_CONN"
	ExtAppKeyWaitQueue             = "WAITQUE_DEPTH"
	ExtAppKeyReqPerSec             = "REQ_PER_SEC"
	ExtAppKeyReqTotal              = "TOT_REQS"
)

// LiteSpeedReport
type LiteSpeedReport struct {
	error             error
	Version           string
	Uptime            float64
	NetworkReport     map[string]float64
	ConnectionReport  map[string]float64
	VirtualHostReport map[string]map[string]float64
	ExtAppReports     map[string]map[string]map[string]map[string]float64
}

// New return a new instance of real time report and error.
func New(path string) (*LiteSpeedReport, error) {
	reportFiles, err := searchReportFiles(path)
	if err != nil {
		return nil, err
	}

	counter := len(reportFiles)
	ch := make(chan *LiteSpeedReport, counter)
	defer close(ch)
	done := make(chan interface{})
	defer close(done)

	loadReportFiles(done, ch, reportFiles)
	sumReportData(done, ch, counter)

	r := <-ch
	return r, r.error
}

// Search Real TIme Report Files.
func searchReportFiles(path string) ([]string, error) {
	files, err := ioutil.ReadDir(path)
	if err != nil {
		return nil, err
	}

	var reportFiles []string
	for _, file := range files {
		if file.IsDir() || !strings.HasPrefix(file.Name(), reportFileNamePrefix) {
			continue
		}
		reportFiles = append(reportFiles, filepath.Join(path, file.Name()))
	}
	return reportFiles, nil
}

func loadReportFiles(done <-chan interface{}, ch chan<- *LiteSpeedReport, reportFiles []string) {
	for _, reportFile := range reportFiles {
		go func(filePath string) {
			select {
			case <-done:
				return
			case ch <- load(filePath):
			}
		}(reportFile)
	}
}

func load(filePath string) *LiteSpeedReport {
	fp, err := os.Open(filePath)
	if err != nil {
		return &LiteSpeedReport{error: err}
	}
	defer fp.Close()

	v := &LiteSpeedReport{
		NetworkReport:     make(map[string]float64),
		ConnectionReport:  make(map[string]float64),
		VirtualHostReport: make(map[string]map[string]float64),
		ExtAppReports:     make(map[string]map[string]map[string]map[string]float64),
	}
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		NewLineParser(scanner.Text()).parse(v)
		if v.error != nil {
			break
		}
	}
	return v
}

func sumReportData(done <-chan interface{}, ch chan *LiteSpeedReport, counter int) {
	for counter > 1 {
		report1 := <-ch
		counter--
		report2 := <-ch
		// be offset. counter-- ; counter++;

		go func(a, b *LiteSpeedReport) {
			select {
			case <-done:
				return
			case ch <- sum(a, b):
			}
		}(report1, report2)
	}
}

func sum(a, b *LiteSpeedReport) *LiteSpeedReport {
	// if error exist. return only error.
	if a.error != nil || b.error != nil {
		v := &LiteSpeedReport{}
		if a.error == nil {
			v.error = b.error
		} else if b.error == nil {
			v.error = a.error
		} else {
			v.error = errors.New(fmt.Sprintf("%s\n--------------%s", a.error.Error(), b.error.Error()))
		}
		return v
	}
	// merge map value.
	mergeSingleMap(a.NetworkReport, b.NetworkReport)
	mergeSingleMap(a.ConnectionReport, b.ConnectionReport)
	mergeDoubleMap(a.VirtualHostReport, b.VirtualHostReport)
	mergeQuadrupleMap(a.ExtAppReports, b.ExtAppReports)
	return a
}
<file_sep>package collector

import (
	"sync"

	"github.com/prometheus/client_golang/prometheus"

	"github.com/myokoo/litespeed_exporter/pkg/rtreport"
)

const (
	namespace = "litespeed" // For Prometheus metrics.
)

var (
	errorDesc = prometheus.NewDesc(
		prometheus.BuildFQName(namespace, "", "up"),
		"Whether the realtime report could be read", nil, nil,
	)
	upteimeDesc = prometheus.NewDesc(
		prometheus.BuildFQName(namespace, "", "uptime_seconds_total"),
		"Current uptime in seconds.", nil, nil,
	)
)

type Exporter struct {
	mutex      sync.Mutex
	reportPath *string
	scrapers   []Scraper
}

func New(path *string) *Exporter {
	return &Exporter{
		reportPath: path,
		scrapers: []Scraper{
			connection{},
			network{},
			virtualHost{},
			extApp{},
		},
	}
}

// Describe implements prometheus.Collector.
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
	ch <- upteimeDesc
}

// Collect implements prometheus.Collector.
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
	e.mutex.Lock()
	defer e.mutex.Unlock()

	report, err := rtreport.New(*e.reportPath)
	if err != nil {
		ch <- metricsIsLitespeedUp(float64(0))
		return
	}
	ch <- metricsIsLitespeedUp(float64(1))
	ch <- prometheus.MustNewConstMetric(upteimeDesc, prometheus.CounterValue, report.Uptime)

	for _, scraper := range e.scrapers {
		scraper.scrape(ch, report)
	}
}

func metricsIsLitespeedUp(i float64) prometheus.Metric {
	return prometheus.MustNewConstMetric(errorDesc, prometheus.GaugeValue, i)
}

func newMetric(namespace, subsystem, name, help string, label []string, metricType prometheus.ValueType, value float64, labelValues ...string) prometheus.Metric {
	return prometheus.MustNewConstMetric(prometheus.NewDesc(prometheus.BuildFQName(namespace, subsystem, name), help, label, nil), metricType, value, labelValues...)
}
<file_sep>## 0.1.6 / 2021-10-05
### Change
- Corrected some spelling/grammar

## 0.1.5 / 2021-05-04
## Add
- Added support for port number in virtual host name
### Change
- Corrected spelling
- Use idomatic naming
- made error messages more clear

## 0.1.4 / 2019-11-05
### Add
- added 'litespeed_virtual_host_requests_per_sec' metrics
- added 'litespeed_external_applicationt_requests_per_sec' metrics

## 0.1.3 / 2019-11-05
### Change
- refactored code

## 0.1.2 / 2019-08-29
### Fixed
- fixed a problem where old metrics data remained

## 0.1.1 / 2019-08-27
### Change
- LiteSpeedReport.Version to string from float64

## 0.1.0 / 2019-08-27

Initial release
<file_sep>package collector

import (
	"github.com/prometheus/client_golang/prometheus"

	"github.com/myokoo/litespeed_exporter/pkg/rtreport"
)

var (
	vhostLabels = []string{"vhost"}
	vName       = "virtual_host"
)

type virtualHost struct{}

func (v virtualHost) scrape(ch chan<- prometheus.Metric, report *rtreport.LiteSpeedReport) {
	for vhost, valueMap := range report.VirtualHostReport {
		for key, value := range valueMap {
			switch key {
			case rtreport.VHostReportKeyProcessing:
				ch <- newMetric(
					namespace, vName, "running_processe",
					"The number of running processes by vhost.",
					vhostLabels, prometheus.GaugeValue, value, vhost,
				)
			case rtreport.VhostReportKeyReqPerSec:
				ch <- newMetric(
					namespace, vName, "requests_per_sec",
					"The total requests per second by vhost.",
					vhostLabels, prometheus.GaugeValue, value, vhost,
				)
			case rtreport.VHostReportKeyReqTotal:
				ch <- newMetric(
					namespace, vName, "requests_total",
					"The total requests by vhost.",
					vhostLabels, prometheus.GaugeValue, value, vhost,
				)
			case rtreport.VHostReportKeyStaticHits:
				ch <- newMetric(
					namespace, vName, "hists_total",
					"The number of static requests by vhost.",
					vhostLabels, prometheus.GaugeValue, value, vhost,
				)
			case rtreport.VHostReportKeyPubCacheHits:
				ch <- newMetric(
					namespace, vName, "public_cache_hists_total",
					"The number of public cache hits by vhost.",
					vhostLabels, prometheus.GaugeValue, value, vhost,
				)
			case rtreport.VHostReportKeyPteCacheHits:
				ch <- newMetric(
					namespace, vName, "private_cache_hists_total",
					"The number of private cache hits by vhost.",
					vhostLabels, prometheus.GaugeValue, value, vhost,
				)
			}
		}
	}
}
