feat(metrics): expose query-metrics and metric-names-list MCP tools #190930
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Rust CI | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: [master, main] | |
| pull_request: | |
| merge_group: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| CARGO_TERM_COLOR: always | |
| UV_HTTP_TIMEOUT: 120 | |
| jobs: | |
| # Job to decide if we should run rust ci | |
| # See https://github.com/dorny/paths-filter#conditional-execution for more details | |
| changes: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| if: github.repository == 'PostHog/posthog' | |
| name: Determine need to run Rust checks | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| # Set job outputs to values from filter step | |
| outputs: | |
| rust: ${{ steps.filter.outputs.rust || 'true' }} | |
| steps: | |
| # For pull requests it's not necessary to checkout the code, but we | |
| # also want this to run on master so we need to checkout | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| clean: false | |
| - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| id: app-token | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| with: | |
| client-id: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_APP_ID }} | |
| private-key: ${{ secrets.GH_APP_POSTHOG_PATHS_FILTER_PRIVATE_KEY }} | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| id: filter | |
| if: github.event_name != 'push' # Run all tests on master push | |
| with: | |
| token: ${{ steps.app-token.outputs.token || github.token }} | |
| filters: | | |
| rust: | |
| # Avoid running rust tests for irrelevant changes | |
| - 'rust/**' | |
| - 'proto/**' | |
| - '.github/workflows/ci-rust.yml' | |
| - '.github/workflows/rust.yml' | |
| - '.github/workflows/rust-docker-build.yml' | |
| - '.github/actions/setup-protoc/**' | |
| - '.github/actions/setup-sccache/**' | |
| - 'posthog/management/commands/setup_test_environment.py' | |
| - 'posthog/migrations/**' | |
| - 'ee/migrations/**' | |
| - 'docker-compose.dev.yml' | |
| # Hypercache contract: Python serializer changes may break Rust deserialization | |
| - 'posthog/api/feature_flag.py' | |
| - 'posthog/models/feature_flag/flags_cache.py' | |
| - 'posthog/models/feature_flag/feature_flag.py' | |
| - 'rust/feature-flags/tests/fixtures/hypercache_contract.json' | |
| # Compute affected crates and test shards. Only runs when Rust changes | |
| # are detected, keeping the lightweight `changes` job fast for non-Rust PRs. | |
| affected: | |
| name: Compute affected Rust crates | |
| needs: changes | |
| if: needs.changes.outputs.rust == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| outputs: | |
| matrix: ${{ steps.shards.outputs.matrix }} | |
| affected_crates: ${{ steps.affected.outputs.crates }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| filter: blob:none | |
| sparse-checkout: | | |
| rust/ | |
| proto/ | |
| .github/rust-images.yml | |
| .github/actions/rust-compute-affected/ | |
| sparse-checkout-cone-mode: false | |
| clean: false | |
| - name: Install rust | |
| uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 | |
| with: | |
| toolchain: 1.91.1 | |
| - name: Compute affected crates | |
| id: affected | |
| uses: ./.github/actions/rust-compute-affected | |
| with: | |
| event-name: ${{ github.event_name }} | |
| pr-base-sha: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} | |
| push-before-sha: ${{ github.event.before }} | |
| rebuild-all: ${{ github.event_name == 'workflow_dispatch' && 'true' || 'false' }} | |
| # Bin-pack affected packages into balanced test shards. | |
| - name: Compute test shards | |
| id: shards | |
| env: | |
| AFFECTED_CRATES: ${{ steps.affected.outputs.crates }} | |
| shell: python3 {0} | |
| run: | | |
| import json, math, os | |
| affected = set(json.loads(os.environ.get("AFFECTED_CRATES", "[]"))) | |
| packages = { | |
| "assignment-coordination": 11, | |
| "batch-import-worker": 124, | |
| "capture": 169, | |
| "capture-logs": 108, | |
| "common-alloc": 13, | |
| "common-compression": 6, | |
| "common-cookieless": 42, | |
| "common-database": 13, | |
| "common-dns": 10, | |
| "common-geoip": 13, | |
| "common-hypercache": 94, | |
| "common-kafka": 67, | |
| "common-metrics": 6, | |
| "common-profiler": 51, | |
| "common-s3": 31, | |
| "cyclotron-core": 17, | |
| "cyclotron-janitor": 139, | |
| "cyclotron-node": 31, | |
| "cymbal": 168, | |
| "embedding-worker": 25, | |
| "feature-flags": 181, | |
| "health": 27, | |
| "hogvm": 36, | |
| "ingestion-consumer": 13, | |
| "k8s-awareness": 107, | |
| "kafka-assigner": 84, | |
| "kafka-deduplicator": 211, | |
| "lifecycle": 17, | |
| "limiters": 25, | |
| "personhog-coordination": 24, | |
| "personhog-leader": 115, | |
| "personhog-replica": 140, | |
| "personhog-router": 38, | |
| "posthog-symbol-data": 19, | |
| "property-defs-rs": 136, | |
| "property-vals-rs": 5, | |
| "serve-metrics": 14, | |
| } | |
| # Filter to only affected crates when selective builds are active. | |
| # Unknown crates (not in packages dict) get a default weight so new | |
| # services are never silently dropped from CI. | |
| DEFAULT_WEIGHT = 60 | |
| if affected: | |
| unknown = affected - packages.keys() | |
| if unknown: | |
| print(f"WARNING: affected crates missing from weight table (using {DEFAULT_WEIGHT}s default): {', '.join(sorted(unknown))}") | |
| print("Add them to the packages dict in ci-rust.yml with measured weights.") | |
| for crate in unknown: | |
| packages[crate] = DEFAULT_WEIGHT | |
| packages = {k: v for k, v in packages.items() if k in affected} | |
| print(f"Selective build: testing {len(packages)} of affected crates") | |
| else: | |
| print("Full build: testing all crates") | |
| if not packages: | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write('matrix={"include":[]}\n') | |
| raise SystemExit(0) | |
| # Weights are approximate total durations (compile + test) in seconds, | |
| # calibrated against 2-week avg CI job times (last updated: 2026-05-07). | |
| # TARGET_MINUTES controls max wall-clock per shard; shard count is derived. | |
| TARGET_MINUTES = 8 | |
| target_seconds = TARGET_MINUTES * 60 | |
| total = sum(packages.values()) | |
| num_shards = max(1, math.ceil(total / target_seconds)) | |
| # Greedy bin-packing: largest first into lightest bucket | |
| buckets = [[] for _ in range(num_shards)] | |
| times = [0] * num_shards | |
| for pkg, t in sorted(packages.items(), key=lambda x: -x[1]): | |
| lightest = min(range(num_shards), key=lambda i: times[i]) | |
| buckets[lightest].append(pkg) | |
| times[lightest] += t | |
| matrix = { | |
| "include": [ | |
| {"packages": " ".join(sorted(b))} | |
| for b in buckets | |
| if b | |
| ] | |
| } | |
| for i, b in enumerate(buckets): | |
| if b: | |
| print(f"Shard {i + 1} ({times[i]}s): {', '.join(sorted(b))}") | |
| print(f"\nTotal: {total}s across {num_shards} shards (target ≤{target_seconds}s/shard)") | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write(f"matrix={json.dumps(matrix)}\n") | |
| build: | |
| name: Build Rust services (depot-ubuntu-22.04-4) | |
| needs: [changes, affected] | |
| if: needs.changes.outputs.rust == 'true' | |
| runs-on: depot-ubuntu-22.04-4 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| defaults: | |
| run: | |
| working-directory: rust | |
| steps: | |
| # Checkout project code | |
| # Use sparse checkout to only select files in rust directory | |
| # Turning off cone mode ensures that files in the project root are not included during checkout | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: | | |
| rust/ | |
| proto/ | |
| .github/actions/setup-protoc/ | |
| .github/actions/setup-sccache/ | |
| sparse-checkout-cone-mode: false | |
| clean: false | |
| - name: Install system OpenSSL | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libssl-dev pkg-config | |
| - name: Install protoc | |
| uses: ./.github/actions/setup-protoc | |
| - name: Install rust | |
| uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 | |
| with: | |
| toolchain: 1.91.1 | |
| - name: Set sccache WebDAV key prefix (Cargo.lock hash) | |
| run: echo "SCCACHE_WEBDAV_KEY_PREFIX=cargo-${{ hashFiles('rust/Cargo.lock') }}" >> "$GITHUB_ENV" | |
| - name: Install sccache | |
| uses: ./.github/actions/setup-sccache | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 | |
| with: | |
| shared-key: 'v2-rust-ci' | |
| workspaces: rust | |
| save-if: ${{ github.ref == 'refs/heads/master' }} | |
| - name: Run cargo build | |
| env: | |
| # Use system OpenSSL instead of vendored to avoid assembly build issue (2026-01-13) | |
| OPENSSL_NO_VENDOR: '1' | |
| AFFECTED_CRATES: ${{ needs.affected.outputs.affected_crates }} | |
| run: | | |
| crates=$(echo "$AFFECTED_CRATES" | python3 -c "import json,sys; print(' '.join(f'-p {c}' for c in json.load(sys.stdin)))") | |
| if [ -z "$crates" ]; then | |
| echo "No affected crates — skipping build" | |
| else | |
| echo "Building affected crates: $crates" | |
| cargo build $crates --locked --release | |
| find target/release/ -maxdepth 1 -executable -type f -exec strip {} + | |
| fi | |
| test: | |
| name: Test Rust (${{ matrix.packages }}) | |
| strategy: | |
| matrix: ${{ fromJSON(needs.affected.outputs.matrix) }} | |
| needs: [changes, affected] | |
| if: needs.changes.outputs.rust == 'true' | |
| runs-on: depot-ubuntu-24.04-4 | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| defaults: | |
| run: | |
| working-directory: rust | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| clean: false | |
| - name: Clean up data directories with container permissions | |
| working-directory: . | |
| run: | | |
| # Use docker to clean up files created by containers (from repo root, not rust/) | |
| [ -d "data" ] && docker run --rm -v "$(pwd)/data:/data" alpine sh -c "rm -rf /data/seaweedfs /data/minio" || true | |
| continue-on-error: true | |
| - name: Log in to Docker Hub | |
| if: ${{ env.DOCKERHUB_USERNAME != '' && env.DOCKERHUB_TOKEN != '' }} | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Setup main repo and dependencies | |
| env: | |
| COMPOSE_FILE: ../docker-compose.dev.yml:../docker-compose.profiles.yml | |
| COMPOSE_PROFILES: etcd | |
| WAIT_FOR_DOCKER_LAUNCH_RETRIES: 3 | |
| WAIT_FOR_DOCKER_LAUNCH_RETRY_DELAY: 10 | |
| run: | | |
| ../bin/ci-wait-for-docker launch --down | |
| ../bin/ci-wait-for-docker wait | |
| echo "127.0.0.1 db redis7 kafka clickhouse clickhouse-coordinator objectstorage seaweedfs temporal" | sudo tee -a /etc/hosts | |
| - name: Dump Kafka logs on failure | |
| if: failure() | |
| run: | | |
| docker ps -a || true | |
| docker logs --tail=500 rust-kafka-1 || true | |
| docker inspect rust-kafka-1 || true | |
| # please keep the tag version here in sync with rust-version in rust/*/Cargo.toml | |
| - name: Install rust | |
| uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 | |
| with: | |
| toolchain: 1.91.1 | |
| - name: Install protoc | |
| uses: ./.github/actions/setup-protoc | |
| - name: Set sccache WebDAV key prefix (Cargo.lock hash) | |
| run: echo "SCCACHE_WEBDAV_KEY_PREFIX=cargo-${{ hashFiles('rust/Cargo.lock') }}" >> "$GITHUB_ENV" | |
| - name: Install sccache | |
| uses: ./.github/actions/setup-sccache | |
| - name: Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version-file: 'pyproject.toml' | |
| token: ${{ github.token }} | |
| - name: Install uv | |
| id: setup-uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 | |
| with: | |
| version: '0.11.14' # pinned: unpinned setup-uv calls GH API on every job, exhausts rate limit | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| save-cache: ${{ github.ref == 'refs/heads/master' }} | |
| - name: Install SAML (python3-saml) dependencies | |
| if: steps.setup-uv.outputs.cache-hit != 'true' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install libxml2-dev libxmlsec1-dev libxmlsec1-openssl postgresql-client | |
| - name: Install python dependencies | |
| run: | | |
| UV_PROJECT_ENVIRONMENT=$pythonLocation uv sync --frozen --dev --directory .. | |
| - name: Install sqlx-cli | |
| uses: ./.github/actions/setup-sqlx-cli | |
| - name: Set up databases | |
| env: | |
| DEBUG: 'true' | |
| TEST: 'true' | |
| SECRET_KEY: 'abcdef' # unsafe - for testing only | |
| DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog' | |
| run: cd ../ && python manage.py setup_test_environment | |
| - name: Run sqlx migrations | |
| env: | |
| DATABASE_URL: 'postgres://posthog:posthog@localhost:5432/posthog_persons' | |
| run: | | |
| sqlx migrate run --source persons_migrations/ | |
| DATABASE_URL='postgres://posthog:posthog@localhost:5432/test_posthog' sqlx migrate run --source behavioral_cohorts_migrations/ | |
| - name: Download MaxMind Database | |
| run: | | |
| cd ../ && ./bin/download-mmdb | |
| - name: Run cargo test | |
| env: | |
| RUST_BACKTRACE: 1 | |
| # Set up dual database environment for feature-flags service | |
| PERSONS_READ_DATABASE_URL: ${{ contains(format(' {0} ', matrix.packages), ' feature-flags ') && 'postgres://posthog:posthog@localhost:5432/posthog_persons' || '' }} | |
| PERSONS_WRITE_DATABASE_URL: ${{ contains(format(' {0} ', matrix.packages), ' feature-flags ') && 'postgres://posthog:posthog@localhost:5432/posthog_persons' || '' }} | |
| run: | | |
| for pkg in ${{ matrix.packages }}; do | |
| echo "::group::Testing $pkg" | |
| # Free cgroups, overlayfs, and network namespaces from previous testcontainers | |
| docker system prune -f --volumes 2>/dev/null || true | |
| extra_args="" | |
| # Limit test threads for packages with sqlx pool exhaustion issues | |
| if [ "$pkg" = "property-defs-rs" ]; then | |
| extra_args="-- --test-threads=4" | |
| fi | |
| cargo test -p "$pkg" $extra_args | |
| echo "::endgroup::" | |
| done | |
| linting: | |
| name: Lint Rust services (depot-ubuntu-22.04-4) | |
| needs: changes | |
| if: needs.changes.outputs.rust == 'true' | |
| runs-on: depot-ubuntu-22.04-4 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| defaults: | |
| run: | |
| working-directory: rust | |
| steps: | |
| # Checkout project code | |
| # Use sparse checkout to only select files in rust directory | |
| # Turning off cone mode ensures that files in the project root are not included during checkout | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: | | |
| rust/ | |
| proto/ | |
| .github/actions/setup-protoc/ | |
| .github/actions/setup-sccache/ | |
| sparse-checkout-cone-mode: false | |
| clean: false | |
| - name: Install system OpenSSL | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libssl-dev pkg-config | |
| - name: Install protoc | |
| uses: ./.github/actions/setup-protoc | |
| - name: Install rust | |
| uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 | |
| with: | |
| toolchain: 1.91.1 | |
| components: clippy,rustfmt | |
| - name: Set sccache WebDAV key prefix (Cargo.lock hash) | |
| run: echo "SCCACHE_WEBDAV_KEY_PREFIX=cargo-${{ hashFiles('rust/Cargo.lock') }}" >> "$GITHUB_ENV" | |
| - name: Install sccache | |
| uses: ./.github/actions/setup-sccache | |
| - name: Cache Rust dependencies | |
| uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 | |
| with: | |
| shared-key: 'v2-rust-ci' | |
| workspaces: rust | |
| save-if: ${{ github.ref == 'refs/heads/master' }} | |
| - name: Check format | |
| run: cargo fmt -- --check | |
| - name: Run clippy | |
| env: | |
| # Use system OpenSSL instead of vendored to avoid assembly build issue (2026-01-13) | |
| OPENSSL_NO_VENDOR: '1' | |
| run: cargo clippy --all-targets --all-features -- -D warnings | |
| - name: Run cargo check | |
| env: | |
| # Use system OpenSSL instead of vendored to avoid assembly build issue (2026-01-13) | |
| OPENSSL_NO_VENDOR: '1' | |
| run: cargo check --all-features | |
| - name: Install cargo-binstall | |
| uses: cargo-bins/cargo-binstall@5cbf019d8cb9b9d5b086218c41458ea35d817691 # main | |
| - name: Install cargo-shear | |
| # --force is required: Swatinem/rust-cache restores ~/.cargo/.crates2.json | |
| # (which records cargo-shear as installed) but not the binary itself, so | |
| # without --force binstall short-circuits ("already installed") and the | |
| # next step fails with `error: no such command: shear`. | |
| run: cargo binstall --no-confirm --force cargo-shear@1.1.12 | |
| - name: Run cargo shear | |
| run: cargo shear | |
| # Collate job - single required status check for branch protection. | |
| # Individual jobs skip entirely when no Rust changes, saving runner time. | |
| rust_tests: | |
| needs: [changes, affected, build, test, linting] | |
| name: Rust Tests Pass | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: always() | |
| permissions: {} | |
| steps: | |
| - name: Check all Rust jobs | |
| run: | | |
| # Fail if change detection or affected computation failed | |
| if [[ "${{ needs.changes.result }}" == "failure" ]]; then | |
| echo "Change detection job failed." | |
| exit 1 | |
| fi | |
| # Pass if no Rust changes detected (jobs were skipped) | |
| if [[ "${{ needs.changes.outputs.rust }}" != "true" ]]; then | |
| echo "Rust checks were skipped (no relevant changes)" | |
| exit 0 | |
| fi | |
| if [[ "${{ needs.affected.result }}" == "failure" ]]; then | |
| echo "Affected crate computation failed." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.build.result }}" != "success" && "${{ needs.build.result }}" != "skipped" ]]; then | |
| echo "Rust build failed." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.test.result }}" != "success" && "${{ needs.test.result }}" != "skipped" ]]; then | |
| echo "Rust tests failed." | |
| exit 1 | |
| fi | |
| if [[ "${{ needs.linting.result }}" != "success" && "${{ needs.linting.result }}" != "skipped" ]]; then | |
| echo "Rust linting failed." | |
| exit 1 | |
| fi | |
| echo "All Rust checks passed." |