Skip to content

feat(metrics): expose query-metrics and metric-names-list MCP tools #190930

feat(metrics): expose query-metrics and metric-names-list MCP tools

feat(metrics): expose query-metrics and metric-names-list MCP tools #190930

Workflow file for this run

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."