diff --git a/examples/go/cron/go.mod b/examples/go/cron/go.mod index 4e017f938c..0c26ca05b9 100644 --- a/examples/go/cron/go.mod +++ b/examples/go/cron/go.mod @@ -4,7 +4,7 @@ go 1.23.0 replace github.com/TBD54566975/ftl => ../../.. -require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 +require github.com/TBD54566975/ftl v0.382.0 require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect diff --git a/examples/go/echo/go.mod b/examples/go/echo/go.mod index c73f9de75a..ffa2c65088 100644 --- a/examples/go/echo/go.mod +++ b/examples/go/echo/go.mod @@ -4,7 +4,7 @@ go 1.23.0 replace github.com/TBD54566975/ftl => ../../.. -require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 +require github.com/TBD54566975/ftl v0.382.0 require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect diff --git a/examples/go/http/go.mod b/examples/go/http/go.mod index 1ec6eff0e4..a01270f8d6 100644 --- a/examples/go/http/go.mod +++ b/examples/go/http/go.mod @@ -4,7 +4,7 @@ go 1.23.0 replace github.com/TBD54566975/ftl => ../../.. -require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 +require github.com/TBD54566975/ftl v0.382.0 require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect diff --git a/examples/go/pubsub/go.mod b/examples/go/pubsub/go.mod index b091d9f224..6f7eb2bae9 100644 --- a/examples/go/pubsub/go.mod +++ b/examples/go/pubsub/go.mod @@ -5,7 +5,7 @@ go 1.23.0 replace github.com/TBD54566975/ftl => ../../.. require ( - github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 + github.com/TBD54566975/ftl v0.382.0 golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f ) diff --git a/examples/go/time/go.mod b/examples/go/time/go.mod index 3acf836d90..da6f95db46 100644 --- a/examples/go/time/go.mod +++ b/examples/go/time/go.mod @@ -4,7 +4,7 @@ go 1.23.0 replace github.com/TBD54566975/ftl => ../../.. -require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 +require github.com/TBD54566975/ftl v0.382.0 require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect 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..fac023b6be --- /dev/null +++ b/internal/dev/grafana.go @@ -0,0 +1,57 @@ +package dev + +import ( + "context" + "fmt" + "net" + + "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} + for _, port := range ports { + if l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)); err != nil { + return fmt.Errorf("port %d is already in use", port) + } else if err = l.Close(); err != nil { + return fmt.Errorf("failed to close listener: %w", err) + } + } + 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)