Skip to content
This repository was archived by the owner on Mar 18, 2025. It is now read-only.

Commit 0f8ea3e

Browse files
committed
Return an error in case of bad syntax
1 parent e3241b8 commit 0f8ea3e

File tree

2 files changed

+332
-8
lines changed

2 files changed

+332
-8
lines changed

]

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package remotewrite
2+
3+
import (
4+
"crypto/tls"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"strconv"
9+
"strings"
10+
"time"
11+
12+
"github.com/grafana/xk6-output-prometheus-remote/pkg/remote"
13+
"go.k6.io/k6/lib/types"
14+
"gopkg.in/guregu/null.v3"
15+
)
16+
17+
const (
18+
defaultServerURL = "http://localhost:9090/api/v1/write"
19+
defaultTimeout = 5 * time.Second
20+
defaultPushInterval = 5 * time.Second
21+
defaultMetricPrefix = "k6_"
22+
)
23+
24+
//nolint:gochecknoglobals
25+
var defaultTrendStats = []string{"p(99)"}
26+
27+
// Config contains the configuration for the Output.
28+
type Config struct {
29+
// ServerURL contains the absolute ServerURL for the Write endpoint where to flush the time series.
30+
ServerURL null.String `json:"url"`
31+
32+
// Headers contains additional headers that should be included in the HTTP requests.
33+
Headers map[string]string `json:"headers"`
34+
35+
// InsecureSkipTLSVerify skips TLS client side checks.
36+
InsecureSkipTLSVerify null.Bool `json:"insecureSkipTLSVerify"`
37+
38+
// Username is the User for Basic Auth.
39+
Username null.String `json:"username"`
40+
41+
// Password is the Password for the Basic Auth.
42+
Password null.String `json:"password"`
43+
44+
// PushInterval defines the time between flushes. The Output will wait the set time
45+
// before push a new set of time series to the endpoint.
46+
PushInterval types.NullDuration `json:"pushInterval"`
47+
48+
// TrendAsNativeHistogram defines if the mapping for metrics defined as Trend type
49+
// should map to a Prometheus' Native Histogram.
50+
TrendAsNativeHistogram null.Bool `json:"trendAsNativeHistogram"`
51+
52+
// TrendStats defines the stats to flush for Trend metrics.
53+
//
54+
// TODO: should we support K6_SUMMARY_TREND_STATS?
55+
TrendStats []string `json:"trendStats"`
56+
57+
StaleMarkers null.Bool `json:"staleMarkers"`
58+
}
59+
60+
// NewConfig creates an Output's configuration.
61+
func NewConfig() Config {
62+
return Config{
63+
ServerURL: null.StringFrom(defaultServerURL),
64+
InsecureSkipTLSVerify: null.BoolFrom(false),
65+
Username: null.NewString("", false),
66+
Password: null.NewString("", false),
67+
PushInterval: types.NullDurationFrom(defaultPushInterval),
68+
Headers: make(map[string]string),
69+
TrendStats: defaultTrendStats,
70+
StaleMarkers: null.BoolFrom(false),
71+
}
72+
}
73+
74+
// RemoteConfig creates a configuration for the HTTP Remote-write client.
75+
func (conf Config) RemoteConfig() (*remote.HTTPConfig, error) {
76+
hc := remote.HTTPConfig{
77+
Timeout: defaultTimeout,
78+
}
79+
80+
// if at least valid user was configured, use basic auth
81+
if conf.Username.Valid {
82+
hc.BasicAuth = &remote.BasicAuth{
83+
Username: conf.Username.String,
84+
Password: conf.Password.String,
85+
}
86+
}
87+
88+
hc.TLSConfig = &tls.Config{
89+
InsecureSkipVerify: conf.InsecureSkipTLSVerify.Bool, //nolint:gosec
90+
}
91+
92+
if len(conf.Headers) > 0 {
93+
hc.Headers = make(http.Header)
94+
for k, v := range conf.Headers {
95+
hc.Headers.Add(k, v)
96+
}
97+
}
98+
return &hc, nil
99+
}
100+
101+
// Apply merges applied Config into base.
102+
func (conf Config) Apply(applied Config) Config {
103+
if applied.ServerURL.Valid {
104+
conf.ServerURL = applied.ServerURL
105+
}
106+
107+
if applied.InsecureSkipTLSVerify.Valid {
108+
conf.InsecureSkipTLSVerify = applied.InsecureSkipTLSVerify
109+
}
110+
111+
if applied.Username.Valid {
112+
conf.Username = applied.Username
113+
}
114+
115+
if applied.Password.Valid {
116+
conf.Password = applied.Password
117+
}
118+
119+
if applied.PushInterval.Valid {
120+
conf.PushInterval = applied.PushInterval
121+
}
122+
123+
if applied.TrendAsNativeHistogram.Valid {
124+
conf.TrendAsNativeHistogram = applied.TrendAsNativeHistogram
125+
}
126+
127+
if applied.StaleMarkers.Valid {
128+
conf.StaleMarkers = applied.StaleMarkers
129+
}
130+
131+
if len(applied.Headers) > 0 {
132+
for k, v := range applied.Headers {
133+
conf.Headers[k] = v
134+
}
135+
}
136+
137+
if len(applied.TrendStats) > 0 {
138+
conf.TrendStats = make([]string, len(applied.TrendStats))
139+
copy(conf.TrendStats, applied.TrendStats)
140+
}
141+
142+
return conf
143+
}
144+
145+
// GetConsolidatedConfig combines the options' values from the different sources
146+
// and returns the merged options. The Order of precedence used is documented
147+
// in the k6 Documentation https://k6.io/docs/using-k6/k6-options/how-to/#order-of-precedence.
148+
func GetConsolidatedConfig(jsonRawConf json.RawMessage, env map[string]string, _ string) (Config, error) {
149+
result := NewConfig()
150+
if jsonRawConf != nil {
151+
jsonConf, err := parseJSON(jsonRawConf)
152+
if err != nil {
153+
return result, fmt.Errorf("parse JSON options failed: %w", err)
154+
}
155+
result = result.Apply(jsonConf)
156+
}
157+
158+
if len(env) > 0 {
159+
envConf, err := parseEnvs(env)
160+
if err != nil {
161+
return result, fmt.Errorf("parse environment variables options failed: %w", err)
162+
}
163+
result = result.Apply(envConf)
164+
}
165+
166+
// TODO: define a way for defining Output's options
167+
// then support them.
168+
// url is the third GetConsolidatedConfig's argument which is omitted for now
169+
//nolint:gocritic
170+
//
171+
//if url != "" {
172+
//urlConf, err := parseArg(url)
173+
//if err != nil {
174+
//return result, fmt.Errorf("parse argument string options failed: %w", err)
175+
//}
176+
//result = result.Apply(urlConf)
177+
//}
178+
179+
return result, nil
180+
}
181+
182+
func parseEnvs(env map[string]string) (Config, error) {
183+
c := Config{
184+
Headers: make(map[string]string),
185+
}
186+
187+
getEnvBool := func(env map[string]string, name string) (null.Bool, error) {
188+
if v, vDefined := env[name]; vDefined {
189+
b, err := strconv.ParseBool(v)
190+
if err != nil {
191+
return null.NewBool(false, false), err
192+
}
193+
194+
return null.BoolFrom(b), nil
195+
}
196+
return null.NewBool(false, false), nil
197+
}
198+
199+
getEnvMap := func(env map[string]string, prefix string) map[string]string {
200+
result := make(map[string]string)
201+
for ek, ev := range env {
202+
if strings.HasPrefix(ek, prefix) {
203+
k := strings.TrimPrefix(ek, prefix)
204+
result[k] = ev
205+
}
206+
}
207+
return result
208+
}
209+
210+
if pushInterval, pushIntervalDefined := env["K6_PROMETHEUS_RW_PUSH_INTERVAL"]; pushIntervalDefined {
211+
if err := c.PushInterval.UnmarshalText([]byte(pushInterval)); err != nil {
212+
return c, err
213+
}
214+
}
215+
216+
if url, urlDefined := env["K6_PROMETHEUS_RW_SERVER_URL"]; urlDefined {
217+
c.ServerURL = null.StringFrom(url)
218+
}
219+
220+
if b, err := getEnvBool(env, "K6_PROMETHEUS_RW_INSECURE_SKIP_TLS_VERIFY"); err != nil {
221+
return c, err
222+
} else if b.Valid {
223+
c.InsecureSkipTLSVerify = b
224+
}
225+
226+
if user, userDefined := env["K6_PROMETHEUS_RW_USERNAME"]; userDefined {
227+
c.Username = null.StringFrom(user)
228+
}
229+
230+
if password, passwordDefined := env["K6_PROMETHEUS_RW_PASSWORD"]; passwordDefined {
231+
c.Password = null.StringFrom(password)
232+
}
233+
234+
envHeaders := getEnvMap(env, "K6_PROMETHEUS_RW_HEADERS_")
235+
for k, v := range envHeaders {
236+
c.Headers[k] = v
237+
}
238+
239+
if headers, headersDefined := env["K6_PROMETHEUS_RW_HTTP_HEADERS"]; headersDefined {
240+
for _, kvPair := range strings.Split(headers, ",") {
241+
header := strings.Split(kvPair, ":")
242+
if len(header) != 2 {
243+
return nil, fmt.Errorf("the provided header (%s) does not respect the expected format <header key>:<value>")
244+
}
245+
c.Headers[header[0]] = header[1]
246+
}
247+
}
248+
249+
if b, err := getEnvBool(env, "K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM"); err != nil {
250+
return c, err
251+
} else if b.Valid {
252+
c.TrendAsNativeHistogram = b
253+
}
254+
255+
if b, err := getEnvBool(env, "K6_PROMETHEUS_RW_STALE_MARKERS"); err != nil {
256+
return c, err
257+
} else if b.Valid {
258+
c.StaleMarkers = b
259+
}
260+
261+
if trendStats, trendStatsDefined := env["K6_PROMETHEUS_RW_TREND_STATS"]; trendStatsDefined {
262+
c.TrendStats = strings.Split(trendStats, ",")
263+
}
264+
265+
return c, nil
266+
}
267+
268+
// parseJSON parses the supplied JSON into a Config.
269+
func parseJSON(data json.RawMessage) (Config, error) {
270+
var c Config
271+
err := json.Unmarshal(data, &c)
272+
return c, err
273+
}
274+
275+
// parseArg parses the supplied string of arguments into a Config.
276+
func parseArg(text string) (Config, error) {
277+
var c Config
278+
opts := strings.Split(text, ",")
279+
280+
for _, opt := range opts {
281+
r := strings.SplitN(opt, "=", 2)
282+
if len(r) != 2 {
283+
return c, fmt.Errorf("couldn't parse argument %q as option", opt)
284+
}
285+
key, v := r[0], r[1]
286+
switch key {
287+
case "url":
288+
c.ServerURL = null.StringFrom(v)
289+
case "insecureSkipTLSVerify":
290+
if err := c.InsecureSkipTLSVerify.UnmarshalText([]byte(v)); err != nil {
291+
return c, fmt.Errorf("insecureSkipTLSVerify value must be true or false, not %q", v)
292+
}
293+
case "username":
294+
c.Username = null.StringFrom(v)
295+
case "password":
296+
c.Password = null.StringFrom(v)
297+
case "pushInterval":
298+
if err := c.PushInterval.UnmarshalText([]byte(v)); err != nil {
299+
return c, err
300+
}
301+
case "trendAsNativeHistogram":
302+
if err := c.TrendAsNativeHistogram.UnmarshalText([]byte(v)); err != nil {
303+
return c, fmt.Errorf("trendAsNativeHistogram value must be true or false, not %q", v)
304+
}
305+
306+
// TODO: add the support for trendStats
307+
// strvals doesn't support the same format used by --summary-trend-stats
308+
// using the comma as the separator, because it is already used for
309+
// dividing the keys.
310+
//nolint:gocritic
311+
//
312+
//if v, ok := params["trendStats"].(string); ok && len(v) > 0 {
313+
//c.TrendStats = strings.Split(v, ",")
314+
//}
315+
316+
default:
317+
if !strings.HasPrefix(key, "headers.") {
318+
return c, fmt.Errorf("%q is an unknown option's key", r[0])
319+
}
320+
if c.Headers == nil {
321+
c.Headers = make(map[string]string)
322+
}
323+
c.Headers[strings.TrimPrefix(key, "headers.")] = v
324+
}
325+
}
326+
327+
return c, nil
328+
}

pkg/remotewrite/config.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ func GetConsolidatedConfig(jsonRawConf json.RawMessage, env map[string]string, _
180180
}
181181

182182
func parseEnvs(env map[string]string) (Config, error) {
183-
var c Config
183+
c := Config{
184+
Headers: make(map[string]string),
185+
}
184186

185187
getEnvBool := func(env map[string]string, name string) (null.Bool, error) {
186188
if v, vDefined := env[name]; vDefined {
@@ -231,20 +233,14 @@ func parseEnvs(env map[string]string) (Config, error) {
231233

232234
envHeaders := getEnvMap(env, "K6_PROMETHEUS_RW_HEADERS_")
233235
for k, v := range envHeaders {
234-
if c.Headers == nil {
235-
c.Headers = make(map[string]string)
236-
}
237236
c.Headers[k] = v
238237
}
239238

240239
if headers, headersDefined := env["K6_PROMETHEUS_RW_HTTP_HEADERS"]; headersDefined {
241-
if c.Headers == nil {
242-
c.Headers = make(map[string]string)
243-
}
244240
for _, kvPair := range strings.Split(headers, ",") {
245241
header := strings.Split(kvPair, ":")
246242
if len(header) != 2 {
247-
continue
243+
return c, fmt.Errorf("the provided header (%s) does not respect the expected format <header key>:<value>", kvPair)
248244
}
249245
c.Headers[header[0]] = header[1]
250246
}

0 commit comments

Comments
 (0)