Skip to content

Commit adca8c0

Browse files
authored
feat(go): more config options (#795)
1 parent b9a3d2e commit adca8c0

File tree

11 files changed

+844
-507
lines changed

11 files changed

+844
-507
lines changed

cmd/healthcheck/cmd.go

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,65 @@ package healthcheck
22

33
import (
44
"context"
5+
"crypto/tls"
56
"fmt"
67
"log/slog"
78
"net/http"
89
"strings"
910

10-
"github.com/grafana/grafana-image-renderer/cmd/config"
11+
"github.com/grafana/grafana-image-renderer/pkg/config"
1112
"github.com/urfave/cli/v3"
1213
)
1314

1415
func NewCmd() *cli.Command {
1516
return &cli.Command{
16-
Name: "healthcheck",
17-
Usage: "Check the server is running and is healthy.",
18-
Flags: []cli.Flag{
19-
&cli.StringFlag{
20-
Name: "addr",
21-
Usage: "The address to listen on for HTTP requests.",
22-
Value: ":8081",
23-
Sources: config.FromConfig("server.addr", "SERVER_ADDR"),
24-
},
25-
},
17+
Name: "healthcheck",
18+
Usage: "Check the server is running and is healthy.",
19+
Flags: config.ServerFlags(),
2620
Action: run,
2721
}
2822
}
2923

3024
func run(ctx context.Context, c *cli.Command) error {
31-
addr := c.String("addr")
32-
if strings.HasPrefix(addr, ":") {
33-
addr = "http://localhost" + addr
34-
} else if !strings.HasPrefix(addr, "http://") && !strings.HasPrefix(addr, "https://") {
35-
addr = "http://" + addr
25+
serverConfig, err := config.ServerConfigFromCommand(c)
26+
if err != nil {
27+
return fmt.Errorf("failed to parse server config: %w", err)
3628
}
3729

38-
req, err := http.NewRequestWithContext(ctx, "GET", addr+"/healthz", nil)
30+
scheme := "http"
31+
if serverConfig.CertificateFile != "" {
32+
scheme = "https"
33+
}
34+
35+
var host string
36+
if strings.HasPrefix(serverConfig.Addr, ":") {
37+
host = "localhost" + serverConfig.Addr
38+
} else if !strings.HasPrefix(serverConfig.Addr, "http://") && !strings.HasPrefix(serverConfig.Addr, "https://") {
39+
host = serverConfig.Addr
40+
}
41+
42+
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s://%s/healthz", scheme, host), nil)
3943
if err != nil {
4044
return fmt.Errorf("failed to create health check request: %w", err)
4145
}
4246
req.Header = http.Header{
4347
"User-Agent": []string{"grafana-image-renderer/Grafana Labs"},
4448
}
4549

46-
resp, err := http.DefaultClient.Do(req)
50+
client := &http.Client{
51+
Transport: &http.Transport{
52+
TLSClientConfig: &tls.Config{
53+
InsecureSkipVerify: true,
54+
},
55+
},
56+
}
57+
58+
resp, err := client.Do(req)
4759
if err != nil {
4860
return fmt.Errorf("failed to perform health check request: %w", err)
4961
}
50-
defer func() {
51-
// We don't care about the body, so we can ignore closing errors, too.
52-
_ = resp.Body.Close()
53-
}()
62+
// We don't care about the body, so we can ignore closing errors, too.
63+
_ = resp.Body.Close()
5464

5565
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
5666
return fmt.Errorf("health check request returned non-2xx status code: %d", resp.StatusCode)

cmd/root.go

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import (
55
"fmt"
66
"log/slog"
77

8-
"github.com/grafana/grafana-image-renderer/cmd/config"
98
"github.com/grafana/grafana-image-renderer/cmd/healthcheck"
109
"github.com/grafana/grafana-image-renderer/cmd/server"
10+
"github.com/grafana/grafana-image-renderer/pkg/config"
1111
"github.com/grafana/grafana-image-renderer/pkg/service"
1212
"github.com/urfave/cli/v3"
1313
"go.opentelemetry.io/otel/trace"
@@ -18,33 +18,15 @@ func NewRootCmd() *cli.Command {
1818
Name: "grafana-image-renderer",
1919
Usage: "A service for Grafana to render images and documents from Grafana websites.",
2020
Version: service.NewVersionService().GetPrettyVersion(),
21-
Flags: []cli.Flag{
22-
&cli.StringFlag{
23-
Name: "log-level",
24-
Usage: "The minimum level to log at (enum: debug, info, warn, error)",
25-
Value: "info",
26-
Sources: config.FromConfig("log.level", "LOG_LEVEL"),
27-
Validator: func(s string) error {
28-
if s != "debug" && s != "info" && s != "warn" && s != "error" {
29-
return fmt.Errorf("invalid log level: %s", s)
30-
}
31-
return nil
32-
},
33-
},
34-
},
21+
Flags: config.LoggingFlags(),
3522
Before: func(ctx context.Context, c *cli.Command) (context.Context, error) {
36-
var leveler slog.Leveler
37-
switch c.String("log-level") {
38-
case "debug":
39-
leveler = slog.LevelDebug
40-
case "info":
41-
leveler = slog.LevelInfo
42-
case "warn":
43-
leveler = slog.LevelWarn
44-
case "error":
45-
leveler = slog.LevelError
46-
default:
47-
return ctx, fmt.Errorf("invalid log level: %s", c.String("log-level"))
23+
loggingConfig, err := config.LoggingConfigFromCommand(c)
24+
if err != nil {
25+
return ctx, fmt.Errorf("failed to parse logging config: %w", err)
26+
}
27+
leveler, err := loggingConfig.Level.ToSlog()
28+
if err != nil {
29+
return ctx, fmt.Errorf("failed to parse log level: %w", err)
4830
}
4931
slog.SetDefault(slog.New(
5032
&traceLogger{

cmd/server/cmd.go

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"fmt"
66
"slices"
77

8-
"github.com/grafana/grafana-image-renderer/cmd/config"
98
"github.com/grafana/grafana-image-renderer/pkg/api"
9+
"github.com/grafana/grafana-image-renderer/pkg/config"
1010
"github.com/grafana/grafana-image-renderer/pkg/metrics"
1111
"github.com/grafana/grafana-image-renderer/pkg/service"
1212
"github.com/grafana/grafana-image-renderer/pkg/traces"
@@ -17,52 +17,27 @@ import (
1717

1818
func NewCmd() *cli.Command {
1919
return &cli.Command{
20-
Name: "server",
21-
Usage: "Run the server part of the service.",
22-
Flags: slices.Concat([]cli.Flag{
23-
&cli.StringFlag{
24-
Name: "addr",
25-
Usage: "The address to listen on for HTTP requests.",
26-
Category: "Server",
27-
Value: ":8081",
28-
Sources: config.FromConfig("server.addr", "SERVER_ADDR"),
29-
},
30-
&cli.StringSliceFlag{
31-
Name: "auth-token",
32-
Usage: "The X-Auth-Token header value that must be sent to the service to permit requests. May be repeated.",
33-
Category: "Server",
34-
Value: []string{"-"},
35-
Sources: config.FromConfig("auth.token", "AUTH_TOKEN"),
36-
},
37-
38-
&cli.StringFlag{
39-
Name: "browser",
40-
Usage: "The path to the browser's binary. This is resolved against PATH.",
41-
Category: "Browser",
42-
TakesFile: true,
43-
Value: "chromium",
44-
Sources: config.FromConfig("browser.path", "BROWSER_PATH"),
45-
},
46-
&cli.StringSliceFlag{
47-
Name: "browser-flags",
48-
Usage: "Flags to pass to the browser. These are syntaxed `<flag>` or `<flag>=<value>`. No -- should be passed in for the flag; these are implied.",
49-
Category: "Browser",
50-
Sources: config.FromConfig("browser.flags", "BROWSER_FLAGS"),
51-
},
52-
&cli.BoolFlag{
53-
Name: "browser-gpu",
54-
Usage: "Enable GPU support in the browser.",
55-
Category: "Browser",
56-
Sources: config.FromConfig("browser.gpu", "BROWSER_GPU"),
57-
},
58-
}, traces.TracerFlags()),
20+
Name: "server",
21+
Usage: "Run the server part of the service.",
22+
Flags: slices.Concat(config.ServerFlags(), config.TracingFlags(), config.BrowserFlags()),
5923
Action: run,
6024
}
6125
}
6226

6327
func run(ctx context.Context, c *cli.Command) error {
64-
metrics := metrics.NewRegistry()
65-
tracerProvider, err := traces.NewTracerProvider(ctx, c)
28+
serverConfig, err := config.ServerConfigFromCommand(c)
29+
if err != nil {
30+
return fmt.Errorf("failed to parse server config: %w", err)
31+
}
32+
browserConfig, err := config.BrowserConfigFromCommand(c)
33+
if err != nil {
34+
return fmt.Errorf("failed to parse browser config: %w", err)
35+
}
36+
tracingConfig, err := config.TracingConfigFromCommand(c)
37+
if err != nil {
38+
return fmt.Errorf("failed to parse tracing config: %w", err)
39+
}
40+
tracerProvider, err := traces.NewTracerProvider(ctx, tracingConfig)
6641
if err != nil {
6742
return fmt.Errorf("failed to set up tracer: %w", err)
6843
}
@@ -72,13 +47,12 @@ func run(ctx context.Context, c *cli.Command) error {
7247
otel.SetTracerProvider(tracerProvider)
7348
otel.SetTextMapPropagator(propagation.TraceContext{})
7449
}
75-
browser := service.NewBrowserService(c.String("browser"), c.StringSlice("browser-flags"),
76-
service.WithViewport(1000, 500),
77-
service.WithGPU(c.Bool("browser-gpu")))
50+
browser := service.NewBrowserService(browserConfig)
7851
versions := service.NewVersionService()
79-
handler, err := api.NewHandler(metrics, browser, api.AuthTokens(c.StringSlice("auth-token")), versions)
52+
metrics := metrics.NewRegistry()
53+
handler, err := api.NewHandler(metrics, serverConfig, browser, versions)
8054
if err != nil {
8155
return fmt.Errorf("failed to create API handler: %w", err)
8256
}
83-
return api.ListenAndServe(ctx, c.String("addr"), handler)
57+
return api.ListenAndServe(ctx, serverConfig, handler)
8458
}

pkg/api/mux.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55

66
"github.com/grafana/grafana-image-renderer/pkg/api/middleware"
7+
"github.com/grafana/grafana-image-renderer/pkg/config"
78
"github.com/grafana/grafana-image-renderer/pkg/service"
89
"github.com/prometheus/client_golang/prometheus"
910
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -15,8 +16,8 @@ func NewHandler(
1516
prometheus.Gatherer
1617
prometheus.Registerer
1718
},
19+
serverConfig config.ServerConfig,
1820
browser *service.BrowserService,
19-
tokens AuthTokens,
2021
versions *service.VersionService,
2122
) (http.Handler, error) {
2223
mux := http.NewServeMux()
@@ -26,8 +27,8 @@ func NewHandler(
2627
mux.Handle("GET /metrics", middleware.TracingFor("promhttp.HandlerFor", promhttp.HandlerFor(metrics, promhttp.HandlerOpts{Registry: metrics})))
2728
mux.Handle("GET /healthz", HandleGetHealthz())
2829
mux.Handle("GET /version", HandleGetVersion(versions, browser))
29-
mux.Handle("GET /render", middleware.RequireAuthToken(middleware.TrustedURL(HandleGetRender(browser)), tokens...))
30-
mux.Handle("GET /render/csv", middleware.RequireAuthToken(middleware.TrustedURL(HandlePostRenderCSV(browser)), tokens...))
30+
mux.Handle("GET /render", middleware.RequireAuthToken(middleware.TrustedURL(HandleGetRender(browser)), serverConfig.AuthTokens...))
31+
mux.Handle("GET /render/csv", middleware.RequireAuthToken(middleware.TrustedURL(HandlePostRenderCSV(browser)), serverConfig.AuthTokens...))
3132
mux.Handle("GET /render/version", HandleGetRenderVersion(versions))
3233

3334
handler := middleware.RequestMetrics(mux)
@@ -36,5 +37,3 @@ func NewHandler(
3637
handler = middleware.Tracing(handler)
3738
return handler, nil
3839
}
39-
40-
type AuthTokens []string

pkg/api/render.go

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package api
22

33
import (
4+
"context"
5+
"errors"
46
"fmt"
57
"log/slog"
68
"net/http"
@@ -64,20 +66,26 @@ func HandleGetRender(browser *service.BrowserService) http.Handler {
6466
}
6567
options = append(options, service.WithViewport(width, height))
6668
if timeout := r.URL.Query().Get("timeout"); timeout != "" {
69+
var dur time.Duration
6770
if regexpOnlyNumbers.MatchString(timeout) {
6871
seconds, err := strconv.Atoi(timeout)
6972
if err != nil {
7073
http.Error(w, fmt.Sprintf("invalid 'timeout' query parameter: %v", err), http.StatusBadRequest)
7174
return
7275
}
73-
options = append(options, service.WithTimeout(time.Duration(seconds)*time.Second))
76+
dur = time.Duration(seconds) * time.Second
7477
} else {
75-
timeout, err := time.ParseDuration(timeout)
78+
var err error
79+
dur, err = time.ParseDuration(timeout)
7680
if err != nil {
7781
http.Error(w, fmt.Sprintf("invalid 'timeout' query parameter: %v", err), http.StatusBadRequest)
7882
return
7983
}
80-
options = append(options, service.WithTimeout(timeout))
84+
}
85+
if dur > 0 {
86+
timeoutCtx, cancelTimeout := context.WithTimeout(ctx, dur)
87+
defer cancelTimeout()
88+
ctx = timeoutCtx
8189
}
8290
}
8391
if scaleFactor := r.URL.Query().Get("deviceScaleFactor"); scaleFactor != "" {
@@ -105,6 +113,7 @@ func HandleGetRender(browser *service.BrowserService) http.Handler {
105113
options = append(options, service.WithCookie("renderKey", renderKey, domain))
106114
}
107115
encoding := r.URL.Query().Get("encoding")
116+
var printer service.Printer
108117
switch encoding {
109118
case "", "pdf":
110119
var printerOpts []service.PDFPrinterOption
@@ -141,7 +150,12 @@ func HandleGetRender(browser *service.BrowserService) http.Handler {
141150
printerOpts = append(printerOpts, service.WithPageRanges(pageRanges))
142151
}
143152

144-
options = append(options, service.WithPDFPrinter(printerOpts...))
153+
var err error
154+
printer, err = service.NewPDFPrinter(printerOpts...)
155+
if err != nil {
156+
http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
157+
return
158+
}
145159

146160
if pdfLandscape := r.URL.Query().Get("pdfLandscape"); pdfLandscape != "" {
147161
options = append(options, service.WithLandscape(pdfLandscape == "true"))
@@ -152,7 +166,13 @@ func HandleGetRender(browser *service.BrowserService) http.Handler {
152166
printerOpts = append(printerOpts, service.WithFullHeight(true))
153167
options = append(options, service.WithViewport(width, 1080)) // add some height to make scrolling faster
154168
}
155-
options = append(options, service.WithPNGPrinter(printerOpts...))
169+
170+
var err error
171+
printer, err = service.NewPNGPrinter(printerOpts...)
172+
if err != nil {
173+
http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
174+
return
175+
}
156176
default:
157177
http.Error(w, fmt.Sprintf("invalid 'encoding' query parameter: %q", encoding), http.StatusBadRequest)
158178
return
@@ -162,9 +182,17 @@ func HandleGetRender(browser *service.BrowserService) http.Handler {
162182
}
163183

164184
start := time.Now()
165-
body, contentType, err := browser.Render(ctx, rawTargetURL, options...)
185+
body, contentType, err := browser.Render(ctx, rawTargetURL, printer, options...)
166186
if err != nil {
167187
MetricRenderDuration.WithLabelValues("error").Observe(time.Since(start).Seconds())
188+
if errors.Is(err, context.DeadlineExceeded) ||
189+
errors.Is(err, context.Canceled) {
190+
http.Error(w, "Request timed out", http.StatusRequestTimeout)
191+
return
192+
} else if errors.Is(err, service.ErrInvalidBrowserOption) {
193+
http.Error(w, fmt.Sprintf("invalid request: %v", err), http.StatusBadRequest)
194+
return
195+
}
168196
slog.ErrorContext(ctx, "failed to render", "error", err)
169197
http.Error(w, "Failed to render", http.StatusInternalServerError)
170198
return

0 commit comments

Comments
 (0)