1/*
2** Zabbix
3** Copyright (C) 2001-2021 Zabbix SIA
4**
5** This program is free software; you can redistribute it and/or modify
6** it under the terms of the GNU General Public License as published by
7** the Free Software Foundation; either version 2 of the License, or
8** (at your option) any later version.
9**
10** This program is distributed in the hope that it will be useful,
11** but WITHOUT ANY WARRANTY; without even the implied warranty of
12** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13** GNU General Public License for more details.
14**
15** You should have received a copy of the GNU General Public License
16** along with this program; if not, write to the Free Software
17** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18**/
19
20package redis
21
22import (
23	"bufio"
24	"encoding/json"
25	"regexp"
26	"strings"
27
28	"zabbix.com/pkg/zbxerr"
29
30	"github.com/mediocregopher/radix/v3"
31)
32
33type infoSection string
34type infoKey string
35type infoKeySpace map[infoKey]interface{}
36type infoExtKey string
37type infoExtKeySpace map[infoExtKey]string
38type redisInfo map[infoSection]infoKeySpace
39
40var redisSlaveMetricRE = regexp.MustCompile(`^slave\d+`)
41
42// parseRedisInfo parses an output of 'INFO' command.
43// https://redis.io/commands/info
44func parseRedisInfo(info string) (res redisInfo, err error) {
45	var (
46		section infoSection
47	)
48
49	scanner := bufio.NewScanner(strings.NewReader(info))
50	res = make(redisInfo)
51
52	for scanner.Scan() {
53		line := scanner.Text()
54
55		if len(line) == 0 {
56			continue
57		}
58
59		// Names of sections are preceded by '#'.
60		if line[0] == '#' {
61			section = infoSection(line[2:])
62			if _, ok := res[section]; !ok {
63				res[section] = make(infoKeySpace)
64			}
65
66			continue
67		}
68
69		// Each parameter represented in the 'key:value' format.
70		kv := strings.SplitN(line, ":", 2)
71		if len(kv) != 2 {
72			continue
73		}
74
75		key := infoKey(kv[0])
76		value := strings.TrimSpace(kv[1])
77
78		// Followed sections has a bit more complicated format.
79		// E.g: dbXXX: keys=XXX,expires=XXX
80		if section == "Keyspace" || section == "Commandstats" ||
81			(section == "Replication" && redisSlaveMetricRE.MatchString(string(key))) {
82			extKeySpace := make(infoExtKeySpace)
83
84			for _, ksParams := range strings.Split(value, ",") {
85				ksParts := strings.Split(ksParams, "=")
86				extKeySpace[infoExtKey(ksParts[0])] = ksParts[1]
87			}
88
89			res[section][key] = extKeySpace
90
91			continue
92		}
93
94		if len(section) == 0 {
95			return nil, zbxerr.ErrorCannotParseResult
96		}
97
98		res[section][key] = value
99	}
100
101	if err = scanner.Err(); err != nil {
102		return nil, err
103	}
104
105	if len(res) == 0 {
106		return nil, zbxerr.ErrorEmptyResult
107	}
108
109	return res, nil
110}
111
112// infoHandler gets an output of 'INFO' command, parses it and returns it in JSON format.
113func infoHandler(conn redisClient, params map[string]string) (interface{}, error) {
114	var res string
115
116	section := infoSection(strings.ToLower(params["Section"]))
117
118	if err := conn.Query(radix.Cmd(&res, "INFO", string(section))); err != nil {
119		return nil, zbxerr.ErrorCannotFetchData.Wrap(err)
120	}
121
122	redisInfo, err := parseRedisInfo(res)
123	if err != nil {
124		return nil, err
125	}
126
127	jsonRes, err := json.Marshal(redisInfo)
128	if err != nil {
129		return nil, zbxerr.ErrorCannotMarshalJSON.Wrap(err)
130	}
131
132	return string(jsonRes), nil
133}
134