diff --git a/.gitignore b/.gitignore index 6299478f..fb0b6e93 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ # created when `just run ex-*` is used /example/share-services/pgweb/data/ /example/simple/data/ +/example/grafana-tempo/data/ /.pre-commit-config.yaml diff --git a/doc/services.md b/doc/services.md index 3038cf1a..648f48ae 100644 --- a/doc/services.md +++ b/doc/services.md @@ -18,6 +18,7 @@ short-title: Services - [[redis-cluster]] - [[zookeeper]]# - [[grafana]]# + - [[tempo]] - [[prometheus]]# - [[cassandra]]# diff --git a/doc/tempo.md b/doc/tempo.md new file mode 100644 index 00000000..ad10efdf --- /dev/null +++ b/doc/tempo.md @@ -0,0 +1,36 @@ +# Grafana Tempo + +[Grafana Tempo](https://grafana.com/docs/tempo/latest/) is an open-source, easy-to-use, and high-scale distributed tracing backend. Tempo lets you search for traces, generate metrics from spans, and link your tracing data with logs and metrics. + +## Getting Started + +```nix +# In `perSystem.process-compose.` +{ + services.tempo."tp1".enable = true; +} +``` + +{#tips} +## Tips & Tricks + +{#usage-with-grafana} +### Usage with Grafana + +To add tempo as a datasource to #[[grafana]], we can use the following config: + +```nix +{ + services.tempo.tp1.enable = true; + services.grafana.gf1 = { + enable = true; + datasources = with config.services.tempo.tp1; [{ + name = "Tempo"; + type = "tempo"; + access = "proxy"; + url = "http://${httpAddress}:${builtins.toString httpPort}"; + }]; + }; + settings.processes."gf1".depends_on."tp1".condition = "process_healthy"; +} +``` diff --git a/example/grafana-tempo/flake.lock b/example/grafana-tempo/flake.lock new file mode 100644 index 00000000..7544fc6b --- /dev/null +++ b/example/grafana-tempo/flake.lock @@ -0,0 +1,105 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1715865404, + "narHash": "sha256-/GJvTdTpuDjNn84j82cU6bXztE0MSkdnTWClUCRub78=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "8dc45382d5206bd292f9c2768b8058a8fd8311d9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1716097317, + "narHash": "sha256-1UMrLtgzielG/Sop6gl6oTSM4pDt7rF9j9VuxhDWDlY=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "8535fb92661f37ff9f0da3007fbc942f7d134b41", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1714640452, + "narHash": "sha256-QBx10+k6JWz6u7VsohfSw8g8hjdBZEf8CFzXH1/1Z94=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/50eb7ecf4cd0a5756d7275c8ba36790e5bd53e33.tar.gz" + } + }, + "process-compose-flake": { + "locked": { + "lastModified": 1715063745, + "narHash": "sha256-kO8gcRHfuKIlsGmFoHUF4lD3CfrRBymIlG2R3OHBEjQ=", + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "rev": "32c069e7ef436b4325ee36503cd02b2863eede53", + "type": "github" + }, + "original": { + "owner": "Platonic-Systems", + "repo": "process-compose-flake", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "process-compose-flake": "process-compose-flake", + "services-flake": "services-flake", + "systems": "systems" + } + }, + "services-flake": { + "locked": { + "dirtyRev": "9fc3e72216aa5b446add37c3c5a3e6de40c0c22a-dirty", + "dirtyShortRev": "9fc3e72-dirty", + "lastModified": 1715582151, + "narHash": "sha256-CWzg2KnFanfz+SLjyDm/IP2TS8t3fdCuvHvAWAfYGR8=", + "type": "git", + "url": "file:///Volumes/Code/services-flake" + }, + "original": { + "type": "git", + "url": "file:///Volumes/Code/services-flake" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/example/grafana-tempo/flake.nix b/example/grafana-tempo/flake.nix new file mode 100644 index 00000000..b2a4114d --- /dev/null +++ b/example/grafana-tempo/flake.nix @@ -0,0 +1,42 @@ +{ + description = "A demo of grafana with tempo"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + systems.url = "github:nix-systems/default"; + process-compose-flake.url = "github:Platonic-Systems/process-compose-flake"; + services-flake.url = "git+file:///Volumes/Code/services-flake"; + }; + outputs = inputs: + inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = import inputs.systems; + imports = [ + inputs.process-compose-flake.flakeModule + ]; + perSystem = { self', pkgs, lib, ... }: { + # `process-compose.foo` will add a flake package output called "foo". + # Therefore, this will add a default package that you can build using + # `nix build` and run using `nix run`. + process-compose."default" = { config, ... }: { + imports = [ + inputs.services-flake.processComposeModules.default + ]; + + services.tempo."tp1".enable = true; + services.grafana."gf1" = { + enable = true; + datasources = with config.services.tempo.tp1; [{ + name = "Tempo"; + type = "tempo"; + access = "proxy"; + url = "http://${httpAddress}:${builtins.toString httpPort}"; + }]; + }; + }; + + devShells.default = pkgs.mkShell { + nativeBuildInputs = [ pkgs.just ]; + }; + }; + }; +} diff --git a/example/grafana-tempo/justfile b/example/grafana-tempo/justfile new file mode 100644 index 00000000..ad604f86 --- /dev/null +++ b/example/grafana-tempo/justfile @@ -0,0 +1,2 @@ +default: + nix run diff --git a/nix/default.nix b/nix/default.nix index 04681e03..3051221c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -17,5 +17,6 @@ in ./prometheus.nix ./pgadmin.nix ./cassandra.nix + ./tempo.nix ]; } diff --git a/nix/grafana.nix b/nix/grafana.nix index 86a3e665..3d317303 100644 --- a/nix/grafana.nix +++ b/nix/grafana.nix @@ -2,6 +2,7 @@ let inherit (lib) types; iniFormat = pkgs.formats.ini { }; + yamlFormat = pkgs.formats.yaml { }; in { options = { @@ -48,6 +49,32 @@ in ''; }; + datasources = lib.mkOption { + type = types.listOf yamlFormat.type; + description = "List of data sources to configure."; + default = [ ]; + example = '' + [ + { + name = "Tempo"; + type = "tempo"; + access = "proxy"; + } + ] + ''; + }; + + deleteDatasources = lib.mkOption { + type = types.listOf yamlFormat.type; + description = "List of data sources to remove."; + default = [ ]; + example = '' + [ + { name = "Tempo"; } + ] + ''; + }; + outputs.settings = lib.mkOption { type = types.deferredModule; internal = true; @@ -63,6 +90,23 @@ in } config.extraConf; grafanaConfigIni = iniFormat.generate "defaults.ini" grafanaConfig; + provisioningConfig = pkgs.stdenv.mkDerivation { + name = "grafana-provisioning"; + datasourcesYaml = yamlFormat.generate "datasources.yaml" { + apiVersion = 1; + deleteDatasources = config.deleteDatasources; + datasources = config.datasources; + }; + buildCommand = '' + mkdir -p $out + mkdir -p $out/alerting + mkdir -p $out/dashboards + mkdir -p $out/datasources + ln -s "$datasourcesYaml" "$out/datasources/datasources.yaml" + mkdir -p $out/notifiers + mkdir -p $out/plugins + ''; + }; startScript = pkgs.writeShellApplication { name = "start-grafana"; runtimeInputs = @@ -73,7 +117,8 @@ in text = '' grafana server --config ${grafanaConfigIni} \ --homepath ${config.package}/share/grafana \ - cfg:paths.data="$(readlink -m ${config.dataDir})" + cfg:paths.data="$(readlink -m ${config.dataDir})" \ + cfg:paths.provisioning="${provisioningConfig}" ''; }; in diff --git a/nix/tempo.nix b/nix/tempo.nix new file mode 100644 index 00000000..34cf2ea0 --- /dev/null +++ b/nix/tempo.nix @@ -0,0 +1,135 @@ +{ pkgs, lib, name, config, ... }: +let + inherit (lib) types; + yamlFormat = pkgs.formats.yaml { }; +in +{ + options = { + description = '' + Configure tempo. + ''; + enable = lib.mkEnableOption name; + + package = lib.mkPackageOption pkgs "tempo" { }; + + httpAddress = lib.mkOption { + type = types.str; + description = "Which address to access tempo from."; + default = "localhost"; + }; + + httpPort = lib.mkOption { + type = types.int; + description = "Which port to run tempo on."; + default = 3200; + }; + + dataDir = lib.mkOption { + type = types.str; + default = "./data/${name}"; + description = "The tempo data directory"; + }; + + extraConfig = lib.mkOption { + type = yamlFormat.type; + default = { }; + description = lib.mdDoc '' + Specify the configuration for Tempo in Nix. + + See https://grafana.com/docs/tempo/latest/configuration/ for available options. + ''; + }; + + extraFlags = lib.mkOption { + type = types.listOf types.str; + default = [ ]; + example = lib.literalExpression + '' + [ "-config.expand-env=true" ] + ''; + description = lib.mdDoc '' + Additional flags to pass to tempo. + ''; + }; + + outputs.settings = lib.mkOption { + type = types.deferredModule; + internal = true; + readOnly = true; + default = { + processes."${name}" = + let + tempoConfig = lib.recursiveUpdate + { + server = { + http_listen_address = config.httpAddress; + http_listen_port = config.httpPort; + }; + storage = { + trace = { + backend = "local"; + wal = { + path = "${config.dataDir}/wal"; + }; + local = { + path = "${config.dataDir}/blocks"; + }; + }; + }; + distributor = { + receivers = { + jaeger = { + protocols = { + thrift_http = null; + grpc = null; + thrift_binary = null; + thrift_compact = null; + }; + }; + zipkin = null; + otlp = { + protocols = { + http = null; + grpc = null; + }; + }; + opencensus = null; + }; + }; + } + config.extraConfig; + tempoConfigYaml = yamlFormat.generate "tempo.yaml" tempoConfig; + startScript = pkgs.writeShellApplication { + name = "start-tempo"; + runtimeInputs = + [ config.package ] ++ + (lib.lists.optionals pkgs.stdenv.isDarwin [ + pkgs.coreutils + ]); + text = '' + tempo --config.file=${tempoConfigYaml} ${lib.escapeShellArgs config.extraFlags} + ''; + }; + in + { + command = startScript; + readiness_probe = { + http_get = { + host = config.httpAddress; + scheme = "http"; + port = config.httpPort; + path = "/ready"; + }; + initial_delay_seconds = 15; + period_seconds = 10; + timeout_seconds = 2; + success_threshold = 1; + failure_threshold = 5; + }; + namespace = name; + availability.restart = "on_failure"; + }; + }; + }; + }; +} diff --git a/nix/tempo_test.nix b/nix/tempo_test.nix new file mode 100644 index 00000000..c2168c74 --- /dev/null +++ b/nix/tempo_test.nix @@ -0,0 +1,25 @@ +{ pkgs, config, ... }: { + services.tempo."tp1" = + { + enable = true; + httpPort = 3200; + }; + + settings.processes.test = + let + cfg = config.services.tempo."tp1"; + in + { + # Tests based on: https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/grafana/basic.nix + command = pkgs.writeShellApplication { + runtimeInputs = [ cfg.package pkgs.gnugrep pkgs.curl pkgs.uutils-coreutils-noprefix ]; + text = + '' + ROOT_URL="http://${cfg.httpAddress}:${builtins.toString cfg.httpPort}"; + curl -sSfN $ROOT_URL/status/version | grep "tempo, version" + ''; + name = "tempo-test"; + }; + depends_on."tp1".condition = "process_healthy"; + }; +} diff --git a/test/flake.nix b/test/flake.nix index dac225bc..dca431c4 100644 --- a/test/flake.nix +++ b/test/flake.nix @@ -46,6 +46,7 @@ "${inputs.services-flake}/nix/prometheus_test.nix" "${inputs.services-flake}/nix/pgadmin_test.nix" "${inputs.services-flake}/nix/cassandra_test.nix" + "${inputs.services-flake}/nix/tempo_test.nix" ]); }; };