From e52b793622f1ce4107ad71324bcd76d503609d65 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 15 Nov 2024 14:03:50 +1100 Subject: [PATCH] feat: auto wire up grafana --- frontend/cli/cmd_schema_import.go | 2 +- frontend/cli/cmd_serve.go | 9 ++++++ internal/container/container.go | 17 +++++------ internal/dev/grafana.go | 50 +++++++++++++++++++++++++++++++ internal/dev/registry.go | 6 ++-- 5 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 internal/dev/grafana.go diff --git a/frontend/cli/cmd_schema_import.go b/frontend/cli/cmd_schema_import.go index 917967b46e..18073fb55d 100644 --- a/frontend/cli/cmd_schema_import.go +++ b/frontend/cli/cmd_schema_import.go @@ -156,7 +156,7 @@ func (s *schemaImportCmd) setup(ctx context.Context) error { return err } - err = container.Run(ctx, "ollama/ollama", ollamaContainerName, s.OllamaPort, 11434, optional.Some(ollamaVolume)) + err = container.Run(ctx, "ollama/ollama", ollamaContainerName, map[int]int{s.OllamaPort: 11434}, optional.Some(ollamaVolume)) if err != nil { return err } diff --git a/frontend/cli/cmd_serve.go b/frontend/cli/cmd_serve.go index 2c2aaae89f..2178ceed07 100644 --- a/frontend/cli/cmd_serve.go +++ b/frontend/cli/cmd_serve.go @@ -51,6 +51,8 @@ type serveCmd struct { ObservabilityConfig observability.Config `embed:"" prefix:"o11y-"` DatabaseImage string `help:"The container image to start for the database" default:"postgres:15.8" env:"FTL_DATABASE_IMAGE" hidden:""` RegistryImage string `help:"The container image to start for the image registry" default:"registry:2" env:"FTL_REGISTRY_IMAGE" hidden:""` + GrafanaImage string `help:"The container image to start for the automatic Grafana instance" default:"grafana/otel-lgtm" env:"FTL_GRAFANA_IMAGE" hidden:""` + DisableGrafana bool `help:"Disable the automatic Grafana that is started if no telemetry collector is specified." default:"false"` controller.CommonConfig provisioner.CommonProvisionerConfig } @@ -133,6 +135,13 @@ func (s *serveCmd) run( if err != nil { return err } + if !s.DisableGrafana && !bool(s.ObservabilityConfig.ExportOTEL) { + err := dev.SetupGrafana(ctx, s.GrafanaImage) + if err != nil { + return fmt.Errorf("failed to setup grafana image: %w", err) + } + os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317") + } wg, ctx := errgroup.WithContext(ctx) diff --git a/internal/container/container.go b/internal/container/container.go index 4bb5a82da8..ae2bbe30c1 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -77,7 +77,7 @@ func Pull(ctx context.Context, imageName string) error { } // Run starts a new detached container with the given image, name, port map, and (optional) volume mount. -func Run(ctx context.Context, image, name string, hostPort, containerPort int, volume optional.Option[string]) error { +func Run(ctx context.Context, image, name string, hostToContainerPort map[int]int, volume optional.Option[string]) error { cli, err := dockerClient.Get(ctx) if err != nil { return err @@ -86,19 +86,17 @@ func Run(ctx context.Context, image, name string, hostPort, containerPort int, v config := container.Config{ Image: image, } + bindings := nat.PortMap{} + for k, v := range hostToContainerPort { + containerNatPort := nat.Port(fmt.Sprintf("%d/tcp", v)) + bindings[containerNatPort] = []nat.PortBinding{{HostPort: strconv.Itoa(k)}} + } - containerNatPort := nat.Port(fmt.Sprintf("%d/tcp", containerPort)) hostConfig := container.HostConfig{ RestartPolicy: container.RestartPolicy{ Name: container.RestartPolicyAlways, }, - PortBindings: nat.PortMap{ - containerNatPort: []nat.PortBinding{ - { - HostPort: strconv.Itoa(hostPort), - }, - }, - }, + PortBindings: bindings, } if v, ok := volume.Get(); ok { hostConfig.Binds = []string{v} @@ -120,6 +118,7 @@ func Run(ctx context.Context, image, name string, hostPort, containerPort int, v // RunDB runs a new detached postgres container with the given name and exposed port. func RunDB(ctx context.Context, name string, port int, image string) error { cli, err := dockerClient.Get(ctx) + if err != nil { return err } diff --git a/internal/dev/grafana.go b/internal/dev/grafana.go new file mode 100644 index 0000000000..9be1ecdbce --- /dev/null +++ b/internal/dev/grafana.go @@ -0,0 +1,50 @@ +package dev + +import ( + "context" + "fmt" + + "github.com/alecthomas/types/optional" + + "github.com/TBD54566975/ftl/internal/container" + "github.com/TBD54566975/ftl/internal/log" +) + +const ftlGrafanaName = "ftl-grafana-1" + +func SetupGrafana(ctx context.Context, image string) error { + logger := log.FromContext(ctx) + + exists, err := container.DoesExist(ctx, ftlGrafanaName, optional.Some(image)) + if err != nil { + return fmt.Errorf("failed to check if container exists: %w", err) + } + // check if port is already in use + ports := []int{3000, 9090, 4317, 4318} + + if !exists { + logger.Debugf("Creating docker container '%s' for grafana", ftlGrafanaName) + + err = container.Run(ctx, image, ftlGrafanaName, map[int]int{3000: 3000, 9090: 9090, 4317: 4317, 4318: 4318}, optional.None[string]()) + if err != nil { + return fmt.Errorf("failed to run grafana container: %w", err) + } + + } else { + // Start the existing container + err = container.Start(ctx, ftlGrafanaName) + if err != nil { + return fmt.Errorf("failed to start existing registry container: %w", err) + } + + logger.Debugf("Reusing existing docker container %s for grafana", ftlGrafanaName) + } + + for _, port := range ports { + err = WaitForPortReady(ctx, port) + if err != nil { + return fmt.Errorf("registry container failed to be healthy: %w", err) + } + } + return nil +} diff --git a/internal/dev/registry.go b/internal/dev/registry.go index e3a38f360f..456b17ddf8 100644 --- a/internal/dev/registry.go +++ b/internal/dev/registry.go @@ -33,7 +33,7 @@ func SetupRegistry(ctx context.Context, image string, port int) error { return fmt.Errorf("failed to close listener: %w", err) } - err = container.Run(ctx, image, ftlRegistryName, port, 5000, optional.None[string]()) + err = container.Run(ctx, image, ftlRegistryName, map[int]int{port: 5000}, optional.None[string]()) if err != nil { return fmt.Errorf("failed to run registry container: %w", err) } @@ -54,7 +54,7 @@ func SetupRegistry(ctx context.Context, image string, port int) error { logger.Debugf("Reusing existing docker container %s on port %d for image registry", ftlRegistryName, port) } - err = WaitForRegistryReady(ctx, port) + err = WaitForPortReady(ctx, port) if err != nil { return fmt.Errorf("registry container failed to be healthy: %w", err) } @@ -62,7 +62,7 @@ func SetupRegistry(ctx context.Context, image string, port int) error { return nil } -func WaitForRegistryReady(ctx context.Context, port int) error { +func WaitForPortReady(ctx context.Context, port int) error { timeout := time.After(10 * time.Minute) retry := time.NewTicker(5 * time.Millisecond)