From 690905410dde713715cedbc4087510d7a18feea0 Mon Sep 17 00:00:00 2001 From: Lyn Nagara Date: Tue, 19 Mar 2024 17:18:35 -0700 Subject: [PATCH 001/287] feat: run outcomes-billing consumer (#2909) --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 90c0f6de023..4d6fc6ab248 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,6 +40,8 @@ x-sentry-defaults: &sentry_defaults <<: *depends_on-default snuba-outcomes-consumer: <<: *depends_on-default + snuba-outcomes-billing-consumer: + <<: *depends_on-default snuba-transactions-consumer: <<: *depends_on-default snuba-subscription-consumer-events: @@ -263,6 +265,9 @@ services: snuba-outcomes-consumer: <<: *snuba_defaults command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + snuba-outcomes-billing-consumer: + <<: *snuba_defaults + command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write --raw-events-topic outcomes-billing # Kafka consumer responsible for feeding transactions data into Clickhouse snuba-transactions-consumer: <<: *snuba_defaults From b3d3ce06da1661eca62a5d1fd5112810d6bbd117 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Wed, 20 Mar 2024 12:27:17 -0700 Subject: [PATCH 002/287] Integration tests in python (#2892) * integration tests in python --- .github/workflows/test.yml | 15 ++ _integration-test/custom-ca-roots/setup.sh | 3 +- _integration-test/custom-ca-roots/teardown.sh | 1 + _integration-test/run.py | 209 ++++++++++++++++++ integration-test.sh | 4 +- requirements-dev.txt | 6 + 6 files changed, 235 insertions(+), 3 deletions(-) mode change 100644 => 100755 _integration-test/custom-ca-roots/setup.sh mode change 100644 => 100755 _integration-test/custom-ca-roots/teardown.sh create mode 100644 _integration-test/run.py create mode 100644 requirements-dev.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21069df20f5..612e2e6669e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,6 +74,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Setup dev environment + run: | + pip install -r requirements-dev.txt + ### pytest-sentry configuration ### + if [ "$GITHUB_REPOSITORY" = "getsentry/self-hosted" ]; then + echo "PYTEST_SENTRY_DSN=${{ env.SELF_HOSTED_TESTING_DSN }}" >> $GITHUB_ENV + echo "PYTEST_SENTRY_TRACES_SAMPLE_RATE=0" >> $GITHUB_ENV + + # This records failures on master to sentry in order to detect flakey tests, as it's + # expected that people have failing tests on their PRs + if [ "$GITHUB_REF" = "refs/heads/master" ]; then + echo "PYTEST_SENTRY_ALWAYS_REPORT=1" >> $GITHUB_ENV + fi + fi + - name: Get Compose run: | # Always remove `docker compose` support as that's the newer version diff --git a/_integration-test/custom-ca-roots/setup.sh b/_integration-test/custom-ca-roots/setup.sh old mode 100644 new mode 100755 index 7cb6dd4fcf0..19be4feb9fb --- a/_integration-test/custom-ca-roots/setup.sh +++ b/_integration-test/custom-ca-roots/setup.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash set -e export COMPOSE_FILE=docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml @@ -42,4 +43,4 @@ openssl req -x509 -newkey rsa:2048 -nodes -days 1 -keyout $TEST_NGINX_CONF_PATH/ cp _integration-test/custom-ca-roots/test.py sentry/test-custom-ca-roots.py -$dc up -d fixture-custom-ca-roots +docker compose --ansi never up -d fixture-custom-ca-roots diff --git a/_integration-test/custom-ca-roots/teardown.sh b/_integration-test/custom-ca-roots/teardown.sh old mode 100644 new mode 100755 index 8b89299666e..35cee3c7a92 --- a/_integration-test/custom-ca-roots/teardown.sh +++ b/_integration-test/custom-ca-roots/teardown.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env bash $dc rm -s -f -v fixture-custom-ca-roots rm -f certificates/test-custom-ca-roots.crt sentry/test-custom-ca-roots.py unset COMPOSE_FILE diff --git a/_integration-test/run.py b/_integration-test/run.py new file mode 100644 index 00000000000..885c115ef96 --- /dev/null +++ b/_integration-test/run.py @@ -0,0 +1,209 @@ +import subprocess +import os +from functools import lru_cache +from bs4 import BeautifulSoup +import httpx +import pytest +import sentry_sdk +import time +import json +import re +from typing import Callable + +SENTRY_CONFIG_PY = "sentry/sentry.conf.py" +SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000/") +TEST_USER = "test@example.com" +TEST_PASS = "test123TEST" +TIMEOUT_SECONDS = 60 + + +def poll_for_response( + request: str, client: httpx.Client, validator: Callable = None +) -> httpx.Response: + for i in range(TIMEOUT_SECONDS): + response = client.get( + request, follow_redirects=True, headers={"Referer": SENTRY_TEST_HOST} + ) + if response.status_code == 200: + if validator is None or validator(response.text): + break + time.sleep(1) + else: + raise AssertionError( + "timeout waiting for response status code 200 or valid data" + ) + return response + + +@lru_cache +def get_sentry_dsn(client: httpx.Client) -> str: + response = poll_for_response( + f"{SENTRY_TEST_HOST}/api/0/projects/sentry/internal/keys/", + client, + lambda x: len(json.loads(x)[0]["dsn"]["public"]) > 0, + ) + sentry_dsn = json.loads(response.text)[0]["dsn"]["public"] + return sentry_dsn + + +@pytest.fixture(scope="session", autouse=True) +def configure_self_hosted_environment(): + subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) + for i in range(TIMEOUT_SECONDS): + try: + response = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) + except httpx.ConnectionError: + time.sleep(1) + else: + if response.status_code == 200: + break + else: + raise AssertionError("timeout waiting for self-hosted to come up") + + # Create test user + subprocess.run( + [ + "docker", + "compose", + "exec", + "web", + "sentry", + "createuser", + "--force-update", + "--superuser", + "--email", + TEST_USER, + "--password", + TEST_PASS, + "--no-input", + ], + check=True, + text=True, + ) + + +@pytest.fixture() +def client_login(): + client = httpx.Client() + response = client.get(SENTRY_TEST_HOST, follow_redirects=True) + parser = BeautifulSoup(response.text, "html.parser") + login_csrf_token = parser.find("input", {"name": "csrfmiddlewaretoken"})["value"] + login_response = client.post( + f"{SENTRY_TEST_HOST}/auth/login/sentry/", + follow_redirects=True, + data={ + "op": "login", + "username": TEST_USER, + "password": TEST_PASS, + "csrfmiddlewaretoken": login_csrf_token, + }, + headers={"Referer": f"{SENTRY_TEST_HOST}/auth/login/sentry/"}, + ) + assert login_response.status_code == 200 + yield (client, login_response) + + +def test_initial_redirect(): + initial_auth_redirect = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) + assert initial_auth_redirect.url == f"{SENTRY_TEST_HOST}/auth/login/sentry/" + + +def test_login(client_login): + client, login_response = client_login + parser = BeautifulSoup(login_response.text, "html.parser") + script_tag = parser.find( + "script", string=lambda x: x and "window.__initialData" in x + ) + assert script_tag is not None + json_data = json.loads(script_tag.text.split("=", 1)[1].strip().rstrip(";")) + assert json_data["isAuthenticated"] is True + assert json_data["user"]["username"] == "test@example.com" + assert json_data["user"]["isSuperuser"] is True + assert login_response.cookies["sc"] is not None + # Set up initial/required settings (InstallWizard request) + client.headers.update({"X-CSRFToken": login_response.cookies["sc"]}) + response = client.put( + f"{SENTRY_TEST_HOST}/api/0/internal/options/?query=is:required", + follow_redirects=True, + headers={"Referer": SENTRY_TEST_HOST}, + data={ + "mail.use-tls": False, + "mail.username": "", + "mail.port": 25, + "system.admin-email": "test@example.com", + "mail.password": "", + "system.url-prefix": SENTRY_TEST_HOST, + "auth.allow-registration": False, + "beacon.anonymous": True, + }, + ) + assert response.status_code == 200 + + +def test_receive_event(client_login): + event_id = None + client, _ = client_login + with sentry_sdk.init(dsn=get_sentry_dsn(client)): + event_id = sentry_sdk.capture_exception(Exception("a failure")) + assert event_id is not None + response = poll_for_response( + f"{SENTRY_TEST_HOST}/api/0/projects/sentry/internal/events/{event_id}/", client + ) + response_json = json.loads(response.text) + assert response_json["eventID"] == event_id + assert response_json["metadata"]["value"] == "a failure" + + +def test_cleanup_crons_running(): + docker_services = subprocess.check_output( + [ + "docker", + "compose", + "--ansi", + "never", + "ps", + "-a", + ], + text=True, + ) + pattern = re.compile( + r"(\-cleanup\s+running)|(\-cleanup[_-].+\s+Up\s+)", re.MULTILINE + ) + cleanup_crons = pattern.findall(docker_services) + assert len(cleanup_crons) > 0 + + +def test_custom_cas(): + try: + subprocess.run(["./_integration-test/custom-ca-roots/setup.sh"], check=True) + subprocess.run( + ["docker", "compose", "--ansi", "never", "run", "--no-deps", "web", "python3", "/etc/sentry/test-custom-ca-roots.py"], check=True + ) + finally: + subprocess.run(["./_integration-test/custom-ca-roots/teardown.sh"], check=True) + + +def test_receive_transaction_events(client_login): + client, _ = client_login + with sentry_sdk.init( + dsn=get_sentry_dsn(client), profiles_sample_rate=1.0, traces_sample_rate=1.0 + ): + + def placeholder_fn(): + sum = 0 + for i in range(5): + sum += i + time.sleep(0.25) + + with sentry_sdk.start_transaction(op="task", name="Test Transactions"): + placeholder_fn() + poll_for_response( + f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=profiles&field=profile.id&project=1&statsPeriod=1h", + client, + lambda x: len(json.loads(x)["data"]) > 0, + ) + poll_for_response( + f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=spansIndexed&field=id&project=1&statsPeriod=1h", + client, + lambda x: len(json.loads(x)["data"]) > 0, + ) diff --git a/integration-test.sh b/integration-test.sh index 7006f80f1fa..29295abc771 100755 --- a/integration-test.sh +++ b/integration-test.sh @@ -15,7 +15,7 @@ export MINIMIZE_DOWNTIME=0 if [[ "$test_option" == "--initial-install" ]]; then echo "Testing initial install" - source _integration-test/run.sh + pytest --reruns 5 _integration-test/run.py source _integration-test/ensure-customizations-not-present.sh source _integration-test/ensure-backup-restore-works.sh elif [[ "$test_option" == "--customizations" ]]; then @@ -34,7 +34,7 @@ EOT echo "Testing in-place upgrade and customizations" export MINIMIZE_DOWNTIME=1 ./install.sh - source _integration-test/run.sh + pytest --reruns 5 _integration-test/run.py source _integration-test/ensure-customizations-work.sh source _integration-test/ensure-backup-restore-works.sh fi diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000000..e87008cfd30 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +sentry-sdk>=1.39.2 +pytest>=8.0.0 +pytest-rerunfailures>=11.0 +pytest-sentry>=0.1.11 +httpx>=0.25.2 +beautifulsoup4>=4.7.1 From 8a5086dec890cc12689015a8ff942b608d7c8693 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 21 Mar 2024 10:27:51 -0700 Subject: [PATCH 003/287] Fix defunct java processes (#2914) revert kafka healthcheck change --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4d6fc6ab248..b0c1be944b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -207,7 +207,7 @@ services: - "sentry-secrets:/etc/kafka/secrets" healthcheck: <<: *healthcheck_defaults - test: ["CMD-SHELL", "/usr/bin/kafka-topics --bootstrap-server kafka:9092 --list"] + test: ["CMD-SHELL", "nc -z localhost 9092"] interval: 10s timeout: 10s retries: 30 From eba22822e4f0a9ee48856ceefe99ebbc1a54a629 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 21 Mar 2024 10:58:30 -0700 Subject: [PATCH 004/287] Port backup tests to python (#2907) * port backup tests to python --- _integration-test/backup.py | 30 ++++++++++++ _integration-test/conftest.py | 47 +++++++++++++++++++ .../ensure-backup-restore-works.sh | 36 -------------- _integration-test/run.py | 36 -------------- integration-test.sh | 4 +- 5 files changed, 79 insertions(+), 74 deletions(-) create mode 100644 _integration-test/backup.py create mode 100644 _integration-test/conftest.py delete mode 100755 _integration-test/ensure-backup-restore-works.sh diff --git a/_integration-test/backup.py b/_integration-test/backup.py new file mode 100644 index 00000000000..cc79cced081 --- /dev/null +++ b/_integration-test/backup.py @@ -0,0 +1,30 @@ +import subprocess +import os +import pytest + +@pytest.fixture() +def setup_env(): + os.environ['SENTRY_DOCKER_IO_DIR'] = os.path.join(os.getcwd(), 'sentry') + os.environ['SKIP_USER_CREATION'] = "1" + +def test_backup(setup_env): + # Docker was giving me permissioning issues when trying to create this file and write to it even after giving read + write access + # to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file + file_path = os.path.join(os.getcwd(), 'sentry', 'backup.json') + sentry_admin_sh = os.path.join(os.getcwd(), 'sentry-admin.sh') + open(file_path, 'a', encoding='utf8').close() + os.chmod(file_path, 0o666) + assert os.path.getsize(file_path) == 0 + subprocess.run([sentry_admin_sh, "export", "global", "/sentry-admin/backup.json", "--no-prompt"], check=True) + assert os.path.getsize(file_path) > 0 + +def test_import(setup_env): + # Bring postgres down and recreate the docker volume + subprocess.run(["docker", "compose", "--ansi", "never", "stop", "postgres"], check=True) + subprocess.run(["docker", "compose", "--ansi", "never", "rm", "-f", "-v", "postgres"], check=True) + subprocess.run(["docker", "volume", "rm", "sentry-postgres"], check=True) + subprocess.run(["docker", "volume", "create", "--name=sentry-postgres"], check=True) + subprocess.run(["docker", "compose", "--ansi", "never", "run", "web", "upgrade", "--noinput"], check=True) + subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) + sentry_admin_sh = os.path.join(os.getcwd(), 'sentry-admin.sh') + subprocess.run([sentry_admin_sh, "import", "global", "/sentry-admin/backup.json", "--no-prompt"], check=True) diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py new file mode 100644 index 00000000000..d83e2cf7043 --- /dev/null +++ b/_integration-test/conftest.py @@ -0,0 +1,47 @@ +import subprocess +import os +import time +import httpx +import pytest + +SENTRY_CONFIG_PY = "sentry/sentry.conf.py" +SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000/") +TEST_USER = "test@example.com" +TEST_PASS = "test123TEST" +TIMEOUT_SECONDS = 60 + + +@pytest.fixture(scope="session", autouse=True) +def configure_self_hosted_environment(): + subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) + for i in range(TIMEOUT_SECONDS): + try: + response = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) + except httpx.ConnectionError: + time.sleep(1) + else: + if response.status_code == 200: + break + else: + raise AssertionError("timeout waiting for self-hosted to come up") + + # Create test user + subprocess.run( + [ + "docker", + "compose", + "exec", + "web", + "sentry", + "createuser", + "--force-update", + "--superuser", + "--email", + TEST_USER, + "--password", + TEST_PASS, + "--no-input", + ], + check=True, + text=True, + ) diff --git a/_integration-test/ensure-backup-restore-works.sh b/_integration-test/ensure-backup-restore-works.sh deleted file mode 100755 index 2b8a13f8ce8..00000000000 --- a/_integration-test/ensure-backup-restore-works.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -ex - -source install/_lib.sh -source install/dc-detect-version.sh - -echo "${_group}Test that backup/restore works..." -echo "Creating backup..." -# Docker was giving me permissioning issues when trying to create this file and write to it even after giving read + write access -# to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file -touch $(pwd)/sentry/backup.json -chmod 666 $(pwd)/sentry/backup.json -SENTRY_DOCKER_IO_DIR=$(pwd)/sentry /bin/bash $(pwd)/sentry-admin.sh export global /sentry-admin/backup.json --no-prompt -if [ ! -s "$(pwd)/sentry/backup.json" ]; then - echo "Backup file is empty" - exit 1 -fi - -# Print backup.json contents -echo "Backup file contents:\n\n" -cat "$(pwd)/sentry/backup.json" - -# Bring postgres down and recreate the docker volume -$dc stop postgres -sleep 5 -$dc rm -f -v postgres -docker volume rm sentry-postgres -export SKIP_USER_CREATION=1 -source install/create-docker-volumes.sh -source install/set-up-and-migrate-database.sh -$dc up -d - -echo "Importing backup..." -SENTRY_DOCKER_IO_DIR=$(pwd)/sentry /bin/bash $(pwd)/sentry-admin.sh import global /sentry-admin/backup.json --no-prompt - -rm $(pwd)/sentry/backup.json diff --git a/_integration-test/run.py b/_integration-test/run.py index 885c115ef96..6162e02f435 100644 --- a/_integration-test/run.py +++ b/_integration-test/run.py @@ -46,42 +46,6 @@ def get_sentry_dsn(client: httpx.Client) -> str: return sentry_dsn -@pytest.fixture(scope="session", autouse=True) -def configure_self_hosted_environment(): - subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) - for i in range(TIMEOUT_SECONDS): - try: - response = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) - except httpx.ConnectionError: - time.sleep(1) - else: - if response.status_code == 200: - break - else: - raise AssertionError("timeout waiting for self-hosted to come up") - - # Create test user - subprocess.run( - [ - "docker", - "compose", - "exec", - "web", - "sentry", - "createuser", - "--force-update", - "--superuser", - "--email", - TEST_USER, - "--password", - TEST_PASS, - "--no-input", - ], - check=True, - text=True, - ) - - @pytest.fixture() def client_login(): client = httpx.Client() diff --git a/integration-test.sh b/integration-test.sh index 29295abc771..33738d23d54 100755 --- a/integration-test.sh +++ b/integration-test.sh @@ -17,7 +17,7 @@ if [[ "$test_option" == "--initial-install" ]]; then echo "Testing initial install" pytest --reruns 5 _integration-test/run.py source _integration-test/ensure-customizations-not-present.sh - source _integration-test/ensure-backup-restore-works.sh + pytest _integration-test/backup.py elif [[ "$test_option" == "--customizations" ]]; then echo "Testing customizations" $dc up -d @@ -36,5 +36,5 @@ EOT ./install.sh pytest --reruns 5 _integration-test/run.py source _integration-test/ensure-customizations-work.sh - source _integration-test/ensure-backup-restore-works.sh + pytest _integration-test/backup.py fi From cb7cc841905a90ea19339f349aa9adfdc45c3949 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 21 Mar 2024 21:35:07 +0100 Subject: [PATCH 005/287] feat(clickhouse): Added max_suspicious_broken_parts to the config.xml (#2853) * feat(clickhouse): Added max_suspicious_broken_parts to the config.xml * refactor(clickhouse): Set default max_suspicious_bronken_parts and Issue reference --------- Co-authored-by: Hubert Deng --- clickhouse/config.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clickhouse/config.xml b/clickhouse/config.xml index 138c87ae7cd..d26bfbb3850 100644 --- a/clickhouse/config.xml +++ b/clickhouse/config.xml @@ -28,5 +28,8 @@ 1 + + 10 From 9b3b9bc3cd360cea6a08ba84a8929e250e170d8d Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 26 Mar 2024 11:14:15 -0700 Subject: [PATCH 006/287] Write Customization tests in python (#2918) * port everything integration test related to python --- .github/workflows/test.yml | 31 ++++++-- _integration-test/conftest.py | 31 +++++++- .../ensure-customizations-not-present.sh | 13 ---- .../ensure-customizations-work.sh | 13 ---- .../{backup.py => test_backup.py} | 9 +-- _integration-test/{run.py => test_run.py} | 74 ++++++++++++++++++- integration-test.sh | 40 ---------- requirements-dev.txt | 2 + 8 files changed, 130 insertions(+), 83 deletions(-) delete mode 100755 _integration-test/ensure-customizations-not-present.sh delete mode 100755 _integration-test/ensure-customizations-work.sh rename _integration-test/{backup.py => test_backup.py} (86%) rename _integration-test/{run.py => test_run.py} (75%) delete mode 100755 integration-test.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 612e2e6669e..195aa79b94e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,11 +55,11 @@ jobs: integration-test: if: github.repository_owner == 'getsentry' runs-on: ubuntu-20.04 - name: "integration test" + name: integration test ${{ matrix.compose_version }} - customizations ${{ matrix.customizations }} strategy: fail-fast: false matrix: - test_type: ["initial-install", "customizations"] + customizations: ["disabled", "enabled"] compose_version: ["v2.0.1", "v2.7.0"] include: - compose_version: "v2.0.1" @@ -68,8 +68,8 @@ jobs: compose_path: "/usr/local/lib/docker/cli-plugins" env: COMPOSE_PROJECT_NAME: self-hosted-${{ strategy.job-index }} - SENTRY_DSN: https://5a620019b5124cbba230a9e62db9b825@o1.ingest.us.sentry.io/6627632 - REPORT_SELF_HOSTED_ISSUES: 1 + REPORT_SELF_HOSTED_ISSUES: 0 + SELF_HOSTED_TESTING_DSN: ${{ vars.SELF_HOSTED_TESTING_DSN }} steps: - name: Checkout uses: actions/checkout@v4 @@ -77,9 +77,10 @@ jobs: - name: Setup dev environment run: | pip install -r requirements-dev.txt + echo "PY_COLORS=1" >> "$GITHUB_ENV" ### pytest-sentry configuration ### if [ "$GITHUB_REPOSITORY" = "getsentry/self-hosted" ]; then - echo "PYTEST_SENTRY_DSN=${{ env.SELF_HOSTED_TESTING_DSN }}" >> $GITHUB_ENV + echo "PYTEST_SENTRY_DSN=$SELF_HOSTED_TESTING_DSN" >> $GITHUB_ENV echo "PYTEST_SENTRY_TRACES_SAMPLE_RATE=0" >> $GITHUB_ENV # This records failures on master to sentry in order to detect flakey tests, as it's @@ -102,13 +103,29 @@ jobs: sudo chmod +x "${{ matrix.compose_path }}/docker-compose" - name: Install self-hosted - run: ./install.sh + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + command: ./install.sh - name: Integration Test - run: ./integration-test.sh --${{ matrix.test_type }} + run: pytest --cov --junitxml=junit.xml --reruns 3 _integration-test/ --customizations=${{ matrix.customizations }} - name: Inspect failure if: failure() run: | docker compose ps docker compose logs + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: getsentry/self-hosted + + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py index d83e2cf7043..cc5ceca3db3 100644 --- a/_integration-test/conftest.py +++ b/_integration-test/conftest.py @@ -10,14 +10,16 @@ TEST_PASS = "test123TEST" TIMEOUT_SECONDS = 60 +def pytest_addoption(parser): + parser.addoption("--customizations", default="disabled") @pytest.fixture(scope="session", autouse=True) -def configure_self_hosted_environment(): +def configure_self_hosted_environment(request): subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) for i in range(TIMEOUT_SECONDS): try: response = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) - except httpx.ConnectionError: + except httpx.NetworkError: time.sleep(1) else: if response.status_code == 200: @@ -25,12 +27,32 @@ def configure_self_hosted_environment(): else: raise AssertionError("timeout waiting for self-hosted to come up") + if request.config.getoption("--customizations") == "enabled": + os.environ['TEST_CUSTOMIZATIONS'] = "enabled" + script_content = '''\ +#!/bin/bash +touch /created-by-enhance-image +apt-get update +apt-get install -y gcc libsasl2-dev python-dev libldap2-dev libssl-dev +''' + + with open('sentry/enhance-image.sh', 'w') as script_file: + script_file.write(script_content) + # Set executable permissions for the shell script + os.chmod('sentry/enhance-image.sh', 0o755) + + # Write content to the requirements.txt file + with open('sentry/requirements.txt', 'w') as req_file: + req_file.write('python-ldap\n') + os.environ['MINIMIZE_DOWNTIME'] = "1" + subprocess.run(["./install.sh"], check=True) # Create test user subprocess.run( [ "docker", "compose", "exec", + "-T", "web", "sentry", "createuser", @@ -45,3 +67,8 @@ def configure_self_hosted_environment(): check=True, text=True, ) + +@pytest.fixture() +def setup_backup_restore_env_variables(): + os.environ['SENTRY_DOCKER_IO_DIR'] = os.path.join(os.getcwd(), 'sentry') + os.environ['SKIP_USER_CREATION'] = "1" diff --git a/_integration-test/ensure-customizations-not-present.sh b/_integration-test/ensure-customizations-not-present.sh deleted file mode 100755 index 78939ae6a06..00000000000 --- a/_integration-test/ensure-customizations-not-present.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -ex - -source install/_lib.sh -source install/dc-detect-version.sh - -# Negated version of ensure-customizations-work.sh, make changes in sync -echo "${_group}Ensure customizations not present" -! $dcr --no-deps web bash -c "if [ ! -e /created-by-enhance-image ]; then exit 1; fi" -! $dcr --no-deps --entrypoint=/etc/sentry/entrypoint.sh sentry-cleanup bash -c "if [ ! -e /created-by-enhance-image ]; then exit 1; fi" -! $dcr --no-deps web python -c "import ldap" -! $dcr --no-deps --entrypoint=/etc/sentry/entrypoint.sh sentry-cleanup python -c "import ldap" -echo "${_endgroup}" diff --git a/_integration-test/ensure-customizations-work.sh b/_integration-test/ensure-customizations-work.sh deleted file mode 100755 index 674cf0710aa..00000000000 --- a/_integration-test/ensure-customizations-work.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -ex - -source install/_lib.sh -source install/dc-detect-version.sh - -# Negated version of ensure-customizations-not-present.sh, make changes in sync -echo "${_group}Ensure customizations work" -$dcr --no-deps web bash -c "if [ ! -e /created-by-enhance-image ]; then exit 1; fi" -$dcr --no-deps --entrypoint=/etc/sentry/entrypoint.sh sentry-cleanup bash -c "if [ ! -e /created-by-enhance-image ]; then exit 1; fi" -$dcr --no-deps web python -c "import ldap" -$dcr --no-deps --entrypoint=/etc/sentry/entrypoint.sh sentry-cleanup python -c "import ldap" -echo "${_endgroup}" diff --git a/_integration-test/backup.py b/_integration-test/test_backup.py similarity index 86% rename from _integration-test/backup.py rename to _integration-test/test_backup.py index cc79cced081..d1252267296 100644 --- a/_integration-test/backup.py +++ b/_integration-test/test_backup.py @@ -1,13 +1,8 @@ import subprocess import os -import pytest -@pytest.fixture() -def setup_env(): - os.environ['SENTRY_DOCKER_IO_DIR'] = os.path.join(os.getcwd(), 'sentry') - os.environ['SKIP_USER_CREATION'] = "1" -def test_backup(setup_env): +def test_backup(setup_backup_restore_env_variables): # Docker was giving me permissioning issues when trying to create this file and write to it even after giving read + write access # to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file file_path = os.path.join(os.getcwd(), 'sentry', 'backup.json') @@ -18,7 +13,7 @@ def test_backup(setup_env): subprocess.run([sentry_admin_sh, "export", "global", "/sentry-admin/backup.json", "--no-prompt"], check=True) assert os.path.getsize(file_path) > 0 -def test_import(setup_env): +def test_import(setup_backup_restore_env_variables): # Bring postgres down and recreate the docker volume subprocess.run(["docker", "compose", "--ansi", "never", "stop", "postgres"], check=True) subprocess.run(["docker", "compose", "--ansi", "never", "rm", "-f", "-v", "postgres"], check=True) diff --git a/_integration-test/run.py b/_integration-test/test_run.py similarity index 75% rename from _integration-test/run.py rename to _integration-test/test_run.py index 6162e02f435..d606c521a46 100644 --- a/_integration-test/run.py +++ b/_integration-test/test_run.py @@ -141,7 +141,18 @@ def test_custom_cas(): try: subprocess.run(["./_integration-test/custom-ca-roots/setup.sh"], check=True) subprocess.run( - ["docker", "compose", "--ansi", "never", "run", "--no-deps", "web", "python3", "/etc/sentry/test-custom-ca-roots.py"], check=True + [ + "docker", + "compose", + "--ansi", + "never", + "run", + "--no-deps", + "web", + "python3", + "/etc/sentry/test-custom-ca-roots.py", + ], + check=True, ) finally: subprocess.run(["./_integration-test/custom-ca-roots/teardown.sh"], check=True) @@ -171,3 +182,64 @@ def placeholder_fn(): client, lambda x: len(json.loads(x)["data"]) > 0, ) + + +def test_customizations(): + commands = [ + [ + "docker", + "compose", + "--ansi", + "never", + "run", + "--no-deps", + "web", + "bash", + "-c", + "if [ ! -e /created-by-enhance-image ]; then exit 1; fi", + ], + [ + "docker", + "compose", + "--ansi", + "never", + "run", + "--no-deps", + "--entrypoint=/etc/sentry/entrypoint.sh", + "sentry-cleanup", + "bash", + "-c", + "if [ ! -e /created-by-enhance-image ]; then exit 1; fi", + ], + [ + "docker", + "compose", + "--ansi", + "never", + "run", + "--no-deps", + "web", + "python", + "-c", + "import ldap", + ], + [ + "docker", + "compose", + "--ansi", + "never", + "run", + "--no-deps", + "--entrypoint=/etc/sentry/entrypoint.sh", + "sentry-cleanup", + "python", + "-c", + "import ldap", + ] + ] + for command in commands: + result = subprocess.run(command, check=False) + if os.getenv("TEST_CUSTOMIZATIONS", "disabled") == "enabled": + assert result.returncode == 0 + else: + assert result.returncode != 0 diff --git a/integration-test.sh b/integration-test.sh deleted file mode 100755 index 33738d23d54..00000000000 --- a/integration-test.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -set -ex - -source install/_lib.sh -source install/detect-platform.sh -source install/dc-detect-version.sh -source install/error-handling.sh - -echo "Reset customizations" -rm -f sentry/enhance-image.sh -rm -f sentry/requirements.txt - -test_option="$1" -export MINIMIZE_DOWNTIME=0 - -if [[ "$test_option" == "--initial-install" ]]; then - echo "Testing initial install" - pytest --reruns 5 _integration-test/run.py - source _integration-test/ensure-customizations-not-present.sh - pytest _integration-test/backup.py -elif [[ "$test_option" == "--customizations" ]]; then - echo "Testing customizations" - $dc up -d - echo "Making customizations" - cat <sentry/enhance-image.sh -#!/bin/bash -touch /created-by-enhance-image -apt-get update -apt-get install -y gcc libsasl2-dev python-dev libldap2-dev libssl-dev -EOT - chmod +x sentry/enhance-image.sh - printf "python-ldap" >sentry/requirements.txt - - echo "Testing in-place upgrade and customizations" - export MINIMIZE_DOWNTIME=1 - ./install.sh - pytest --reruns 5 _integration-test/run.py - source _integration-test/ensure-customizations-work.sh - pytest _integration-test/backup.py -fi diff --git a/requirements-dev.txt b/requirements-dev.txt index e87008cfd30..1a27ff14bda 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,7 @@ +codecov-cli>=0.4.8 sentry-sdk>=1.39.2 pytest>=8.0.0 +pytest-cov>=4.1.0 pytest-rerunfailures>=11.0 pytest-sentry>=0.1.11 httpx>=0.25.2 From c4ae491929170c075bea608941b227a8db81eec2 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 26 Mar 2024 14:57:49 -0700 Subject: [PATCH 007/287] Bump ubuntu version for tests (#2923) * bump ubuntu version used for testing * get rid of codecov cli dependency --- .github/workflows/test.yml | 6 +++--- requirements-dev.txt | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 195aa79b94e..c2b8f700c70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ defaults: jobs: e2e-test: if: github.repository_owner == 'getsentry' - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 name: "Sentry self-hosted end-to-end tests" steps: - name: Checkout @@ -43,7 +43,7 @@ jobs: unit-test: if: github.repository_owner == 'getsentry' - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 name: "unit tests" steps: - name: Checkout @@ -54,7 +54,7 @@ jobs: integration-test: if: github.repository_owner == 'getsentry' - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 name: integration test ${{ matrix.compose_version }} - customizations ${{ matrix.customizations }} strategy: fail-fast: false diff --git a/requirements-dev.txt b/requirements-dev.txt index 1a27ff14bda..7ef178da75a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ -codecov-cli>=0.4.8 sentry-sdk>=1.39.2 pytest>=8.0.0 pytest-cov>=4.1.0 From 20150f06d7a5b3d80135e40a32f7eab15e17f0be Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:16:18 -0400 Subject: [PATCH 008/287] fix(spans): Adds organizations:standalone-span-ingestion flag to default config (#2936) Adds organizations:standalone-span-ingestion flag to default config --- sentry/sentry.conf.example.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 88cb014b8e1..5247cca12db 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -307,6 +307,7 @@ def get_internal_network(): "organizations:performance-screens-view", "organizations:mobile-ttid-ttfd-contribution", "organizations:starfish-mobile-appstart", + "organizations:standalone-span-ingestion", ) # starfish related flags } ) From b063f0f12da9fe676d08dc63bf9b5a852006015e Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Wed, 10 Apr 2024 10:23:45 -0700 Subject: [PATCH 009/287] feat: adds group attributes consumer (#2927) adds group attributes consumer --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index b0c1be944b2..5b7b5b627e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -281,6 +281,9 @@ services: snuba-metrics-consumer: <<: *snuba_defaults command: rust-consumer --storage metrics_raw --consumer-group snuba-metrics-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + snuba-group-attributes-consumer: + <<: *snuba_defaults + command: rust-consumer --storage group_attributes --consumer-group snuba-group-attributes-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write snuba-generic-metrics-distributions-consumer: <<: *snuba_defaults command: rust-consumer --storage generic_metrics_distributions_raw --consumer-group snuba-gen-metrics-distributions-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write From 7c992697af16365a800b9c64c76d17ffceead8fa Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 11 Apr 2024 13:12:19 -0700 Subject: [PATCH 010/287] Use python for e2e tests (#2953) * bump e2e action commit sha --- .github/workflows/test.yml | 2 +- _integration-test/run.sh | 152 ------------------------------------- test.sh | 12 --- 3 files changed, 1 insertion(+), 165 deletions(-) delete mode 100755 _integration-test/run.sh delete mode 100755 test.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2b8f700c70..5dc896fecb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" - name: End to end tests - uses: getsentry/action-self-hosted-e2e-tests@03010bd2963edc1f47b6e5e03167a4bc1433ea36 + uses: getsentry/action-self-hosted-e2e-tests@main with: project_name: self-hosted diff --git a/_integration-test/run.sh b/_integration-test/run.sh deleted file mode 100755 index 482665d549b..00000000000 --- a/_integration-test/run.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash -set -ex - -echo "${_group}Setting up variables and helpers ..." -export SENTRY_TEST_HOST="${SENTRY_TEST_HOST:-http://localhost:9000}" -TEST_USER='test@example.com' -TEST_PASS='test123TEST' -COOKIE_FILE=$(mktemp) - -trap_with_arg cleanup ERR INT TERM EXIT -echo "${_endgroup}" - -echo "${_group}Starting Sentry for tests ..." -# Disable beacon for e2e tests -echo 'SENTRY_BEACON=False' >>$SENTRY_CONFIG_PY -$dc up -d -timeout 90 bash -c 'until $(curl -Isf -o /dev/null $SENTRY_TEST_HOST); do printf '.'; sleep 0.5; done' -# DC exec here is faster, tests run on the slower side and using exec would provide a boost -echo y | $dc exec web sentry createuser --force-update --superuser --email $TEST_USER --password $TEST_PASS -printf "Waiting for Sentry to be up" -echo "" -echo "${_endgroup}" - -echo "${_group}Running tests ..." -get_csrf_token() { awk '$6 == "sc" { print $7 }' $COOKIE_FILE; } -sentry_api_request() { curl -s -H 'Accept: application/json; charset=utf-8' -H "Referer: $SENTRY_TEST_HOST" -H 'Content-Type: application/json' -H "X-CSRFToken: $(get_csrf_token)" -b "$COOKIE_FILE" -c "$COOKIE_FILE" "$SENTRY_TEST_HOST/api/0/$1" ${@:2}; } - -login() { - INITIAL_AUTH_REDIRECT=$(curl -sL -o /dev/null $SENTRY_TEST_HOST -w %{url_effective}) - if [ "$INITIAL_AUTH_REDIRECT" != "$SENTRY_TEST_HOST/auth/login/sentry/" ]; then - echo "Initial /auth/login/ redirect failed, exiting..." - echo "$INITIAL_AUTH_REDIRECT" - exit 1 - fi - - CSRF_TOKEN_FOR_LOGIN=$(curl $SENTRY_TEST_HOST -sL -c "$COOKIE_FILE" | awk -F "['\"]" ' - /csrfmiddlewaretoken/ { - print $4 "=" $6; - exit; - }') - - curl -sL --data-urlencode 'op=login' --data-urlencode "username=$TEST_USER" --data-urlencode "password=$TEST_PASS" --data-urlencode "$CSRF_TOKEN_FOR_LOGIN" "$SENTRY_TEST_HOST/auth/login/sentry/" -H "Referer: $SENTRY_TEST_HOST/auth/login/sentry/" -b "$COOKIE_FILE" -c "$COOKIE_FILE" -} - -LOGIN_RESPONSE=$(login) -declare -a LOGIN_TEST_STRINGS=( - '"isAuthenticated":true' - '"username":"test@example.com"' - '"isSuperuser":true' -) -for i in "${LOGIN_TEST_STRINGS[@]}"; do - echo "Testing '$i'..." - echo "$LOGIN_RESPONSE" | grep "${i}[,}]" >&/dev/null - echo "Pass." -done -echo "${_endgroup}" - -echo "${_group}Running moar tests !!!" -# Set up initial/required settings (InstallWizard request) -export -f sentry_api_request get_csrf_token -sentry_api_request "internal/options/?query=is:required" -X PUT --data '{"mail.use-tls":false,"mail.username":"","mail.port":25,"system.admin-email":"ben@byk.im","mail.password":"","system.url-prefix":"'"$SENTRY_TEST_HOST"'","auth.allow-registration":false,"beacon.anonymous":true}' >/dev/null - -# Hacky way to get around test flakiness for now -sleep 60 -# We ignore the protocol and the host as we already know those -SENTRY_DSN=$(sentry_api_request "projects/sentry/internal/keys/" | awk 'BEGIN { RS=",|:{\n"; FS="\""; } $2 == "public" && $4 ~ "^http" { print $4; exit; }') -DSN_PIECES=($(echo $SENTRY_DSN | sed -ne 's|^https\{0,1\}://\([0-9a-z]\{1,\}\)@[^/]\{1,\}/\([0-9]\{1,\}\)$|\1 \2|p' | tr ' ' '\n')) -SENTRY_KEY=${DSN_PIECES[0]} -PROJECT_ID=${DSN_PIECES[1]} - -TEST_EVENT_ID=$( - export LC_ALL=C - head /dev/urandom | tr -dc "a-f0-9" | head -c 32 -) -# Thanks @untitaker - https://forum.sentry.io/t/how-can-i-post-with-curl-a-sentry-event-which-authentication-credentials/4759/2?u=byk -echo "Creating test event..." -curl -sf --data '{"event_id": "'"$TEST_EVENT_ID"'","level":"error","message":"a failure","extra":{"object":"42"}}' -H 'Content-Type: application/json' -H "X-Sentry-Auth: Sentry sentry_version=7, sentry_key=$SENTRY_KEY, sentry_client=test-bash/0.1" "$SENTRY_TEST_HOST/api/$PROJECT_ID/store/" -o /dev/null - -EVENT_PATH="projects/sentry/internal/events/$TEST_EVENT_ID/" -export SENTRY_TEST_HOST COOKIE_FILE EVENT_PATH -printf "Getting the test event back" -timeout 60 bash -c 'until $(sentry_api_request "$EVENT_PATH" -Isf -X GET -o /dev/null); do printf '.'; sleep 0.5; done' -echo " got it!" - -EVENT_RESPONSE=$(sentry_api_request "$EVENT_PATH") -declare -a EVENT_TEST_STRINGS=( - '"eventID":"'"$TEST_EVENT_ID"'"' - '"message":"a failure"' - '"title":"a failure"' - '"object":"42"' -) -for i in "${EVENT_TEST_STRINGS[@]}"; do - echo "Testing '$i'..." - echo "$EVENT_RESPONSE" | grep "${i}[,}]" >&/dev/null - echo "Pass." -done -echo "${_endgroup}" - -echo "${_group}Ensure cleanup crons are working ..." -$dc ps -a | tee debug.log | grep -E -e '\-cleanup\s+running\s+' -e '\-cleanup[_-].+\s+Up\s+' -# to debug https://github.com/getsentry/self-hosted/issues/1171 -echo '------------------------------------------' -cat debug.log -echo '------------------------------------------' -echo "${_endgroup}" - -echo "${_group}Test custom CAs work ..." -source _integration-test/custom-ca-roots/setup.sh -$dcr --no-deps web python3 /etc/sentry/test-custom-ca-roots.py -source _integration-test/custom-ca-roots/teardown.sh -echo "${_endgroup}" - -echo "${_group}Test that profiling work ..." -echo "Sending a test profile..." -PROFILE_FIXTURE_PATH="$(git rev-parse --show-toplevel)/_integration-test/fixtures/envelope-with-profile" -curl -sf --data-binary @$PROFILE_FIXTURE_PATH -H 'Content-Type: application/x-sentry-envelope' -H "X-Sentry-Auth: Sentry sentry_version=7, sentry_key=$SENTRY_KEY, sentry_client=test-bash/0.1" "$SENTRY_TEST_HOST/api/$PROJECT_ID/envelope/" - -printf "Getting the test profile back" -PROFILE_ID="$(jq -r -n --slurpfile profile $PROFILE_FIXTURE_PATH '$profile[4].event_id')" -PROFILE_PATH="projects/sentry/sentry/profiling/raw_profiles/$PROFILE_ID/" -timeout 60 bash -c 'until sentry_api_request "$PROFILE_PATH" -X GET -o /dev/null; do printf '.'; sleep 0.5; done' -echo " got it!" -echo "${_endgroup}" - -echo "${_group}Test we can extract spans from an event..." -echo "Sending a test span..." -SPAN_FIXTURE_PATH="$(git rev-parse --show-toplevel)/_integration-test/fixtures/envelope-with-transaction" -curl -sf --data-binary @$PROFILE_FIXTURE_PATH -H 'Content-Type: application/x-sentry-envelope' -H "X-Sentry-Auth: Sentry sentry_version=7, sentry_key=$SENTRY_KEY, sentry_client=test-bash/0.1" "$SENTRY_TEST_HOST/api/$PROJECT_ID/envelope/" - -printf "Getting a span back" -TRACE_ID="$(jq -r -n --slurpfile span $SPAN_FIXTURE_PATH '$span[2].contexts.trace.trace_id')" -SPAN_PATH="organizations/sentry/events/" -SPAN_QUERY_PARAMS="-G --data-urlencode dataset=spansIndexed --data-urlencode field=id --data-urlencode project=1 --data-urlencode query=trace:$TRACE_ID --data-urlencode statsPeriod=1h" -sleep 10 -sentry_api_request $SPAN_PATH -X GET $SPAN_QUERY_PARAMS | jq .data[] -e -echo " got it!" -echo "${_endgroup}" - -# Table formatting based on https://stackoverflow.com/a/39144364 -COMPOSE_PS_OUTPUT=$(docker compose ps --format json | jq -r \ - '.[] | - # we only care about running services. geoipupdate and fixture-custom-ca-roots always exits, so we ignore it - select(.State != "running" and .Service != "geoipupdate" and .Service != "fixture-custom-ca-roots") | - # Filter to only show the service name and state - with_entries(select(.key | in({"Service":1, "State":1}))) - ') - -if [[ "$COMPOSE_PS_OUTPUT" ]]; then - echo "Services failed, oh no!" - echo "$COMPOSE_PS_OUTPUT" | jq -rs '["Service","State"], ["-------","-----"], (.[]|[.Service, .State]) | @tsv' - exit 1 -fi diff --git a/test.sh b/test.sh deleted file mode 100755 index c7fbd3a9a70..00000000000 --- a/test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -e - -export MINIMIZE_DOWNTIME=0 -export REPORT_SELF_HOSTED_ISSUES=1 - -# This file runs in https://github.com/getsentry/sentry/blob/fe4795f5eae9e0d7c33e0ecb736c9d1369535eca/docker/cloudbuild.yaml#L59 -source install/_lib.sh -source install/detect-platform.sh -source install/dc-detect-version.sh -source install/error-handling.sh -source _integration-test/run.sh From 140954a1f7a95db544aac26e04fc110fc84d781b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 16 Apr 2024 15:50:03 +0000 Subject: [PATCH 011/287] release: 24.4.0 --- .env | 10 +++++----- CHANGELOG.md | 16 ++++++++++++++++ README.md | 2 +- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..ae6f7627d13 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.4.0 +SNUBA_IMAGE=getsentry/snuba:24.4.0 +RELAY_IMAGE=getsentry/relay:24.4.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.4.0 +VROOM_IMAGE=getsentry/vroom:24.4.0 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c59bae468..9beab03feee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 24.4.0 + +### Various fixes & improvements + +- Use python for e2e tests (#2953) by @hubertdeng123 +- feat: adds group attributes consumer (#2927) by @scefali +- fix(spans): Adds organizations:standalone-span-ingestion flag to default config (#2936) by @edwardgou-sentry +- Bump ubuntu version for tests (#2923) by @hubertdeng123 +- Write Customization tests in python (#2918) by @hubertdeng123 +- feat(clickhouse): Added max_suspicious_broken_parts to the config.xml (#2853) by @victorelec14 +- Port backup tests to python (#2907) by @hubertdeng123 +- Fix defunct java processes (#2914) by @hubertdeng123 +- Integration tests in python (#2892) by @hubertdeng123 +- feat: run outcomes-billing consumer (#2909) by @lynnagara +- Remove duplicate feature flags (#2899) by @JannKleen + ## 24.3.0 ### Various fixes & improvements diff --git a/README.md b/README.md index 13f4f2b8934..a69f73aefb2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.4.0 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 3d63c9b79e7010e451e6fd12a12df2ced51c4a6a Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 16 Apr 2024 16:34:28 +0000 Subject: [PATCH 012/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index ae6f7627d13..8e54a3df2c9 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.4.0 -SNUBA_IMAGE=getsentry/snuba:24.4.0 -RELAY_IMAGE=getsentry/relay:24.4.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.4.0 -VROOM_IMAGE=getsentry/vroom:24.4.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index a69f73aefb2..13f4f2b8934 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.4.0 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From b5237d2a62105f12b9f1d3d564a8690a45f537a0 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 16 Apr 2024 15:51:23 -0700 Subject: [PATCH 013/287] Port last integration tests to python (#2966) * port custom ca cert test to python --- _integration-test/conftest.py | 28 +- _integration-test/custom-ca-roots/setup.sh | 46 ---- _integration-test/custom-ca-roots/teardown.sh | 4 - _integration-test/custom-ca-roots/test.py | 5 +- .../ensure-sentry-admin-works.sh | 27 -- _integration-test/test_backup.py | 64 ++++- _integration-test/test_run.py | 251 ++++++++++++++++-- sentry-admin.sh | 3 +- 8 files changed, 301 insertions(+), 127 deletions(-) delete mode 100755 _integration-test/custom-ca-roots/setup.sh delete mode 100755 _integration-test/custom-ca-roots/teardown.sh delete mode 100644 _integration-test/ensure-sentry-admin-works.sh diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py index cc5ceca3db3..a8a229ac77a 100644 --- a/_integration-test/conftest.py +++ b/_integration-test/conftest.py @@ -1,6 +1,7 @@ -import subprocess import os +import subprocess import time + import httpx import pytest @@ -10,8 +11,10 @@ TEST_PASS = "test123TEST" TIMEOUT_SECONDS = 60 + def pytest_addoption(parser): - parser.addoption("--customizations", default="disabled") + parser.addoption("--customizations", default="disabled") + @pytest.fixture(scope="session", autouse=True) def configure_self_hosted_environment(request): @@ -28,23 +31,23 @@ def configure_self_hosted_environment(request): raise AssertionError("timeout waiting for self-hosted to come up") if request.config.getoption("--customizations") == "enabled": - os.environ['TEST_CUSTOMIZATIONS'] = "enabled" - script_content = '''\ + os.environ["TEST_CUSTOMIZATIONS"] = "enabled" + script_content = """\ #!/bin/bash touch /created-by-enhance-image apt-get update apt-get install -y gcc libsasl2-dev python-dev libldap2-dev libssl-dev -''' +""" - with open('sentry/enhance-image.sh', 'w') as script_file: + with open("sentry/enhance-image.sh", "w") as script_file: script_file.write(script_content) # Set executable permissions for the shell script - os.chmod('sentry/enhance-image.sh', 0o755) + os.chmod("sentry/enhance-image.sh", 0o755) # Write content to the requirements.txt file - with open('sentry/requirements.txt', 'w') as req_file: - req_file.write('python-ldap\n') - os.environ['MINIMIZE_DOWNTIME'] = "1" + with open("sentry/requirements.txt", "w") as req_file: + req_file.write("python-ldap\n") + os.environ["MINIMIZE_DOWNTIME"] = "1" subprocess.run(["./install.sh"], check=True) # Create test user subprocess.run( @@ -68,7 +71,8 @@ def configure_self_hosted_environment(request): text=True, ) + @pytest.fixture() def setup_backup_restore_env_variables(): - os.environ['SENTRY_DOCKER_IO_DIR'] = os.path.join(os.getcwd(), 'sentry') - os.environ['SKIP_USER_CREATION'] = "1" + os.environ["SENTRY_DOCKER_IO_DIR"] = os.path.join(os.getcwd(), "sentry") + os.environ["SKIP_USER_CREATION"] = "1" diff --git a/_integration-test/custom-ca-roots/setup.sh b/_integration-test/custom-ca-roots/setup.sh deleted file mode 100755 index 19be4feb9fb..00000000000 --- a/_integration-test/custom-ca-roots/setup.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -set -e -export COMPOSE_FILE=docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml - -TEST_NGINX_CONF_PATH=_integration-test/custom-ca-roots/nginx -CUSTOM_CERTS_PATH=certificates - -# generate tightly constrained CA -# NB: `-addext` requires LibreSSL 3.1.0+, or OpenSSL (brew install openssl) -openssl req -x509 -new -nodes -newkey rsa:2048 -keyout $TEST_NGINX_CONF_PATH/ca.key \ - -sha256 -days 1 -out $TEST_NGINX_CONF_PATH/ca.crt -batch \ - -subj "/CN=TEST CA *DO NOT TRUST*" \ - -addext "keyUsage = critical, keyCertSign, cRLSign" \ - -addext "nameConstraints = critical, permitted;DNS:self.test" - -## Lines like the following are debug helpers ... -# openssl x509 -in nginx/ca.crt -text -noout - -mkdir -p $CUSTOM_CERTS_PATH -cp $TEST_NGINX_CONF_PATH/ca.crt $CUSTOM_CERTS_PATH/test-custom-ca-roots.crt - -# generate server certificate -openssl req -new -nodes -newkey rsa:2048 -keyout $TEST_NGINX_CONF_PATH/self.test.key \ - -addext "subjectAltName=DNS:self.test" \ - -out $TEST_NGINX_CONF_PATH/self.test.req -batch -subj "/CN=Self Signed with CA Test Server" - -# openssl req -in nginx/self.test.req -text -noout - -openssl x509 -req -in $TEST_NGINX_CONF_PATH/self.test.req -CA $TEST_NGINX_CONF_PATH/ca.crt -CAkey $TEST_NGINX_CONF_PATH/ca.key \ - -extfile <(printf "subjectAltName=DNS:self.test") \ - -CAcreateserial -out $TEST_NGINX_CONF_PATH/self.test.crt -days 1 -sha256 - -# openssl x509 -in nginx/self.test.crt -text -noout - -# sanity check that signed certificate passes OpenSSL's validation -openssl verify -CAfile $TEST_NGINX_CONF_PATH/ca.crt $TEST_NGINX_CONF_PATH/self.test.crt - -# self signed certificate, for sanity check of not just accepting all certs -openssl req -x509 -newkey rsa:2048 -nodes -days 1 -keyout $TEST_NGINX_CONF_PATH/fake.test.key \ - -out $TEST_NGINX_CONF_PATH/fake.test.crt -addext "subjectAltName=DNS:fake.test" -subj "/CN=Self Signed Test Server" - -# openssl x509 -in nginx/fake.test.crt -text -noout - -cp _integration-test/custom-ca-roots/test.py sentry/test-custom-ca-roots.py - -docker compose --ansi never up -d fixture-custom-ca-roots diff --git a/_integration-test/custom-ca-roots/teardown.sh b/_integration-test/custom-ca-roots/teardown.sh deleted file mode 100755 index 35cee3c7a92..00000000000 --- a/_integration-test/custom-ca-roots/teardown.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash -$dc rm -s -f -v fixture-custom-ca-roots -rm -f certificates/test-custom-ca-roots.crt sentry/test-custom-ca-roots.py -unset COMPOSE_FILE diff --git a/_integration-test/custom-ca-roots/test.py b/_integration-test/custom-ca-roots/test.py index 0f9b501f83a..bd4fdadbd85 100644 --- a/_integration-test/custom-ca-roots/test.py +++ b/_integration-test/custom-ca-roots/test.py @@ -1,15 +1,16 @@ import unittest + import requests class CustomCATests(unittest.TestCase): def test_valid_self_signed(self): - self.assertEqual(requests.get("https://self.test/").text, 'ok') + self.assertEqual(requests.get("https://self.test/").text, "ok") def test_invalid_self_signed(self): with self.assertRaises(requests.exceptions.SSLError): requests.get("https://fail.test/") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/_integration-test/ensure-sentry-admin-works.sh b/_integration-test/ensure-sentry-admin-works.sh deleted file mode 100644 index f8f574ec8ae..00000000000 --- a/_integration-test/ensure-sentry-admin-works.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bash -set -ex - -source install/_lib.sh -source install/dc-detect-version.sh - -echo "${_group}Test that sentry-admin works..." - -echo "Global help documentation..." - -global_help_doc=$(/bin/bash --help) -if ! echo "$global_help_doc" | grep -q "^Usage: ./sentry-admin.sh"; then - echo "Assertion failed: Incorrect binary name in global help docs" - exit 1 -fi -if ! echo "$global_help_doc" | grep -q "SENTRY_DOCKER_IO_DIR"; then - echo "Assertion failed: Missing SENTRY_DOCKER_IO_DIR global help doc" - exit 1 -fi - -echo "Command-specific help documentation..." - -command_help_doc=$(/bin/bash permissions --help) -if ! echo "$command_help_doc" | grep -q "^Usage: ./sentry-admin.sh permissions"; then - echo "Assertion failed: Incorrect binary name in command-specific help docs" - exit 1 -fi diff --git a/_integration-test/test_backup.py b/_integration-test/test_backup.py index d1252267296..8c482869e8d 100644 --- a/_integration-test/test_backup.py +++ b/_integration-test/test_backup.py @@ -1,25 +1,69 @@ -import subprocess import os +import subprocess + + +def test_sentry_admin(setup_backup_restore_env_variables): + sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") + output = subprocess.run( + [sentry_admin_sh, "--help"], check=True, capture_output=True, encoding="utf8" + ).stdout + assert "Usage: ./sentry-admin.sh" in output + assert "SENTRY_DOCKER_IO_DIR" in output + + output = subprocess.run( + [sentry_admin_sh, "permissions", "--help"], + check=True, + capture_output=True, + encoding="utf8", + ).stdout + assert "Usage: ./sentry-admin.sh permissions" in output def test_backup(setup_backup_restore_env_variables): # Docker was giving me permissioning issues when trying to create this file and write to it even after giving read + write access # to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file - file_path = os.path.join(os.getcwd(), 'sentry', 'backup.json') - sentry_admin_sh = os.path.join(os.getcwd(), 'sentry-admin.sh') - open(file_path, 'a', encoding='utf8').close() + file_path = os.path.join(os.getcwd(), "sentry", "backup.json") + sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") + open(file_path, "a", encoding="utf8").close() os.chmod(file_path, 0o666) assert os.path.getsize(file_path) == 0 - subprocess.run([sentry_admin_sh, "export", "global", "/sentry-admin/backup.json", "--no-prompt"], check=True) + subprocess.run( + [ + sentry_admin_sh, + "export", + "global", + "/sentry-admin/backup.json", + "--no-prompt", + ], + check=True, + ) assert os.path.getsize(file_path) > 0 + def test_import(setup_backup_restore_env_variables): # Bring postgres down and recreate the docker volume - subprocess.run(["docker", "compose", "--ansi", "never", "stop", "postgres"], check=True) - subprocess.run(["docker", "compose", "--ansi", "never", "rm", "-f", "-v", "postgres"], check=True) + subprocess.run( + ["docker", "compose", "--ansi", "never", "stop", "postgres"], check=True + ) + subprocess.run( + ["docker", "compose", "--ansi", "never", "rm", "-f", "-v", "postgres"], + check=True, + ) subprocess.run(["docker", "volume", "rm", "sentry-postgres"], check=True) subprocess.run(["docker", "volume", "create", "--name=sentry-postgres"], check=True) - subprocess.run(["docker", "compose", "--ansi", "never", "run", "web", "upgrade", "--noinput"], check=True) + subprocess.run( + ["docker", "compose", "--ansi", "never", "run", "web", "upgrade", "--noinput"], + check=True, + ) subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) - sentry_admin_sh = os.path.join(os.getcwd(), 'sentry-admin.sh') - subprocess.run([sentry_admin_sh, "import", "global", "/sentry-admin/backup.json", "--no-prompt"], check=True) + sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") + subprocess.run( + [ + sentry_admin_sh, + "import", + "global", + "/sentry-admin/backup.json", + "--no-prompt", + ], + check=True, + ) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index d606c521a46..84bc027c2b3 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -1,14 +1,22 @@ -import subprocess +import datetime +import json import os +import re +import shutil +import subprocess +import time from functools import lru_cache -from bs4 import BeautifulSoup +from typing import Callable + import httpx import pytest import sentry_sdk -import time -import json -import re -from typing import Callable +from bs4 import BeautifulSoup +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID SENTRY_CONFIG_PY = "sentry/sentry.conf.py" SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000/") @@ -137,25 +145,218 @@ def test_cleanup_crons_running(): assert len(cleanup_crons) > 0 -def test_custom_cas(): - try: - subprocess.run(["./_integration-test/custom-ca-roots/setup.sh"], check=True) - subprocess.run( - [ - "docker", - "compose", - "--ansi", - "never", - "run", - "--no-deps", - "web", - "python3", - "/etc/sentry/test-custom-ca-roots.py", - ], - check=True, +def test_custom_certificate_authorities(): + # Set environment variable + os.environ["COMPOSE_FILE"] = ( + "docker-compose.yml:_integration-test/custom-ca-roots/docker-compose.test.yml" + ) + + test_nginx_conf_path = "_integration-test/custom-ca-roots/nginx" + custom_certs_path = "certificates" + + # Generate tightly constrained CA + ca_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() + ) + + ca_name = x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "TEST CA *DO NOT TRUST*")] + ) + + ca_cert = ( + x509.CertificateBuilder() + .subject_name(ca_name) + .issuer_name(ca_name) + .public_key(ca_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) + .add_extension( + x509.KeyUsage( + digital_signature=False, + key_encipherment=False, + content_commitment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=True, + crl_sign=True, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.NameConstraints([x509.DNSName("self.test")], None), critical=True + ) + .sign(private_key=ca_key, algorithm=hashes.SHA256(), backend=default_backend()) + ) + + ca_key_path = f"{test_nginx_conf_path}/ca.key" + ca_crt_path = f"{test_nginx_conf_path}/ca.crt" + + with open(ca_key_path, "wb") as key_file: + key_file.write( + ca_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + + with open(ca_crt_path, "wb") as cert_file: + cert_file.write(ca_cert.public_bytes(serialization.Encoding.PEM)) + + # Create custom certs path and copy ca.crt + os.makedirs(custom_certs_path, exist_ok=True) + shutil.copyfile(ca_crt_path, f"{custom_certs_path}/test-custom-ca-roots.crt") + # Generate server key and certificate + + self_test_key_path = os.path.join(test_nginx_conf_path, "self.test.key") + self_test_csr_path = os.path.join(test_nginx_conf_path, "self.test.csr") + self_test_cert_path = os.path.join(test_nginx_conf_path, "self.test.crt") + + self_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + self_test_req = ( + x509.CertificateSigningRequestBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, "Self Signed with CA Test Server" + ) + ] + ) + ) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False + ) + .sign(self_test_key, hashes.SHA256()) + ) + + self_test_cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [ + x509.NameAttribute( + NameOID.COMMON_NAME, "Self Signed with CA Test Server" + ) + ] + ) + ) + .issuer_name(ca_cert.issuer) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .public_key(self_test_req.public_key()) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("self.test")]), critical=False + ) + .sign(private_key=ca_key, algorithm=hashes.SHA256()) + ) + + # Save server key, CSR, and certificate + with open(self_test_key_path, "wb") as key_file: + key_file.write( + self_test_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + with open(self_test_csr_path, "wb") as csr_file: + csr_file.write(self_test_req.public_bytes(serialization.Encoding.PEM)) + with open(self_test_cert_path, "wb") as cert_file: + cert_file.write(self_test_cert.public_bytes(serialization.Encoding.PEM)) + + # Generate server key and certificate for fake.test + + fake_test_key_path = os.path.join(test_nginx_conf_path, "fake.test.key") + fake_test_cert_path = os.path.join(test_nginx_conf_path, "fake.test.crt") + + fake_test_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + fake_test_cert = ( + x509.CertificateBuilder() + .subject_name( + x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] + ) + ) + .issuer_name( + x509.Name( + [x509.NameAttribute(NameOID.COMMON_NAME, "Self Signed Test Server")] + ) + ) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.utcnow()) + .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .public_key(fake_test_key.public_key()) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("fake.test")]), critical=False + ) + .sign(private_key=fake_test_key, algorithm=hashes.SHA256()) + ) + + # Save server key and certificate for fake.test + with open(fake_test_key_path, "wb") as key_file: + key_file.write( + fake_test_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + # Our asserts for this test case must be executed within the web container, so we are copying a python test script into the mounted sentry directory + with open(fake_test_cert_path, "wb") as cert_file: + cert_file.write(fake_test_cert.public_bytes(serialization.Encoding.PEM)) + shutil.copyfile( + "_integration-test/custom-ca-roots/test.py", + "sentry/test-custom-ca-roots.py", ) - finally: - subprocess.run(["./_integration-test/custom-ca-roots/teardown.sh"], check=True) + + subprocess.run( + ["docker", "compose", "--ansi", "never", "up", "-d", "fixture-custom-ca-roots"], + check=True, + ) + subprocess.run( + [ + "docker", + "compose", + "--ansi", + "never", + "run", + "--no-deps", + "web", + "python3", + "/etc/sentry/test-custom-ca-roots.py", + ], + check=True, + ) + subprocess.run( + [ + "docker", + "compose", + "--ansi", + "never", + "rm", + "-s", + "-f", + "-v", + "fixture-custom-ca-roots", + ], + check=True, + ) + + # Remove files + os.remove(f"{custom_certs_path}/test-custom-ca-roots.crt") + os.remove("sentry/test-custom-ca-roots.py") + + # Unset environment variable + if "COMPOSE_FILE" in os.environ: + del os.environ["COMPOSE_FILE"] def test_receive_transaction_events(client_login): @@ -235,7 +436,7 @@ def test_customizations(): "python", "-c", "import ldap", - ] + ], ] for command in commands: result = subprocess.run(command, check=False) diff --git a/sentry-admin.sh b/sentry-admin.sh index ff50d9f4750..85705516d27 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -19,7 +19,8 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { - $dc run -v "$VOLUME_MAPPING" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" + output=$($dc run -v "$VOLUME_MAPPING" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1) + echo "$output" } # Function to modify lines starting with `Usage: sentry` to say `Usage: ./sentry-admin.sh` instead. From 922829986f3f2a30e2af8893945dad17b14345f5 Mon Sep 17 00:00:00 2001 From: Edgar Sanchez Date: Wed, 17 Apr 2024 11:18:52 -0500 Subject: [PATCH 014/287] Add example to docker compose version in problem report (#2959) --- .github/ISSUE_TEMPLATE/problem-report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/problem-report.yml b/.github/ISSUE_TEMPLATE/problem-report.yml index d306c56eca5..53ac4ec2acb 100644 --- a/.github/ISSUE_TEMPLATE/problem-report.yml +++ b/.github/ISSUE_TEMPLATE/problem-report.yml @@ -36,6 +36,7 @@ body: placeholder: 2.6.0 ← should look like this (docker compose version) description: | What version of docker compose are you using to run self-hosted? + e.g: (docker compose version) validations: required: true - type: textarea From d586cff03af2acfe01775ee91b3c0365bac20d53 Mon Sep 17 00:00:00 2001 From: Steffen Zieger Date: Wed, 17 Apr 2024 18:38:21 +0200 Subject: [PATCH 015/287] Use docker compose exec to create additional kafka topics (#2904) --- install/create-kafka-topics.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/install/create-kafka-topics.sh b/install/create-kafka-topics.sh index 7f9022e3931..63e0cffa016 100644 --- a/install/create-kafka-topics.sh +++ b/install/create-kafka-topics.sh @@ -1,12 +1,23 @@ echo "${_group}Creating additional Kafka topics ..." -# NOTE: This step relies on `kafka` being available from the previous `snuba-api bootstrap` step +$dc up -d --no-build --no-recreate kafka + +while [ true ]; do + kafka_healthy=$($dc ps kafka | grep 'healthy') + if [ ! -z "$kafka_healthy" ]; then + break + fi + + echo "Kafka container is not healthy, waiting for 30 seconds. If this took too long, abort the installation process, and check your Kafka configuration" + sleep 30s +done + # XXX(BYK): We cannot use auto.create.topics as Confluence and Apache hates it now (and makes it very hard to enable) -EXISTING_KAFKA_TOPICS=$($dcr -T kafka kafka-topics --list --bootstrap-server kafka:9092 2>/dev/null) +EXISTING_KAFKA_TOPICS=$($dc exec -T kafka kafka-topics --list --bootstrap-server kafka:9092 2>/dev/null) NEEDED_KAFKA_TOPICS="ingest-attachments ingest-transactions ingest-events ingest-replay-recordings profiles ingest-occurrences ingest-metrics ingest-performance-metrics ingest-monitors" for topic in $NEEDED_KAFKA_TOPICS; do if ! echo "$EXISTING_KAFKA_TOPICS" | grep -qE "(^| )$topic( |$)"; then - $dcr kafka kafka-topics --create --topic $topic --bootstrap-server kafka:9092 + $dc exec kafka kafka-topics --create --topic $topic --bootstrap-server kafka:9092 echo "" fi done From d80e62d9c71afeba8ccc3cd53ec665fd7eb20a65 Mon Sep 17 00:00:00 2001 From: Matthew T <20070360+mdtro@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:29:16 -0500 Subject: [PATCH 016/287] chore(deps): bump memcached and redis to latest patch versions (#2973) --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5b7b5b627e2..0b26520a9ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,7 +111,7 @@ services: - "sentry-smtp-log:/var/log/exim4" memcached: <<: *restart_policy - image: "memcached:1.6.21-alpine" + image: "memcached:1.6.26-alpine" command: ["-I", "${SENTRY_MAX_EXTERNAL_SOURCEMAP_SIZE:-1M}"] healthcheck: <<: *healthcheck_defaults @@ -119,7 +119,7 @@ services: test: echo stats | nc 127.0.0.1 11211 redis: <<: *restart_policy - image: "redis:6.2.13-alpine" + image: "redis:6.2.14-alpine" healthcheck: <<: *healthcheck_defaults test: redis-cli ping From 2fe5499fd108f052057a1c9636b9028462fa1a9a Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 18 Apr 2024 17:30:54 +0000 Subject: [PATCH 017/287] release: 24.4.1 --- .env | 10 +++++----- CHANGELOG.md | 9 +++++++++ README.md | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..c843e9e7974 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.4.1 +SNUBA_IMAGE=getsentry/snuba:24.4.1 +RELAY_IMAGE=getsentry/relay:24.4.1 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.4.1 +VROOM_IMAGE=getsentry/vroom:24.4.1 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index 9beab03feee..d3bcd69c13d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 24.4.1 + +### Various fixes & improvements + +- chore(deps): bump memcached and redis to latest patch versions (#2973) by @mdtro +- Use docker compose exec to create additional kafka topics (#2904) by @saz +- Add example to docker compose version in problem report (#2959) by @edgariscoding +- Port last integration tests to python (#2966) by @hubertdeng123 + ## 24.4.0 ### Various fixes & improvements diff --git a/README.md b/README.md index 13f4f2b8934..7fc6b8c3ee5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.4.1 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 0f2f276c46f44ee464c3b69048b1d4b5d5cca467 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 18 Apr 2024 18:08:30 +0000 Subject: [PATCH 018/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index c843e9e7974..8e54a3df2c9 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.4.1 -SNUBA_IMAGE=getsentry/snuba:24.4.1 -RELAY_IMAGE=getsentry/relay:24.4.1 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.4.1 -VROOM_IMAGE=getsentry/vroom:24.4.1 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index 7fc6b8c3ee5..13f4f2b8934 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.4.1 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From d59c0aa57c1432af9c5000007c17a87c07935a55 Mon Sep 17 00:00:00 2001 From: Alex Zaslavsky Date: Thu, 18 Apr 2024 11:21:38 -0700 Subject: [PATCH 019/287] Add workstation configuration (#2968) * Add workstation configuration These are prebuilt docker images for spinning up a local self-hosted image on the Google Cloud Workstation project. While primarily intended for internal development at Sentry, in theory these can be used by anyone with GCWS project to create a fresh workstation for developing self-hosted via a remote VSCode connection. Users who have GCWS properly configured will be able to use the forthcoming `workstations ...` command in the `sentry` dev CLI to create, manage, and destroy one-off or long-lived workstations in either the pre-install or post-install configuration. Note that the `sentry workstations ...` CLI has not yet landed in the `sentry` repo - those changes are coming soon! Issue: getsentry/team-ospo#240 * Fix shfmt complaints --- .gitignore | 3 + workstation/200_download-self-hosted.sh | 11 ++ workstation/201_install-self-hosted.sh | 8 + workstation/299_setup-completed.sh | 9 + workstation/README.md | 160 ++++++++++++++++++ workstation/commands.sh | 4 + .../docs/img/create_artifcat_registry.png | Bin 0 -> 59445 bytes workstation/docs/img/create_cluster.png | Bin 0 -> 63658 bytes workstation/docs/img/create_config_1.png | Bin 0 -> 89116 bytes workstation/docs/img/create_config_2.png | Bin 0 -> 105490 bytes workstation/docs/img/create_config_3.png | Bin 0 -> 126889 bytes workstation/docs/img/create_repository.png | Bin 0 -> 107072 bytes workstation/postinstall/Dockerfile | 32 ++++ workstation/preinstall/Dockerfile | 31 ++++ 14 files changed, 258 insertions(+) create mode 100644 workstation/200_download-self-hosted.sh create mode 100644 workstation/201_install-self-hosted.sh create mode 100644 workstation/299_setup-completed.sh create mode 100644 workstation/README.md create mode 100644 workstation/commands.sh create mode 100644 workstation/docs/img/create_artifcat_registry.png create mode 100644 workstation/docs/img/create_cluster.png create mode 100644 workstation/docs/img/create_config_1.png create mode 100644 workstation/docs/img/create_config_2.png create mode 100644 workstation/docs/img/create_config_3.png create mode 100644 workstation/docs/img/create_repository.png create mode 100644 workstation/postinstall/Dockerfile create mode 100644 workstation/preinstall/Dockerfile diff --git a/.gitignore b/.gitignore index b52e82ddf0c..7311a09ae9c 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,6 @@ postgres/wal2json # integration testing _integration-test/custom-ca-roots/nginx/* sentry/test-custom-ca-roots.py + +# OSX minutia +.DS_Store diff --git a/workstation/200_download-self-hosted.sh b/workstation/200_download-self-hosted.sh new file mode 100644 index 00000000000..a07735d7003 --- /dev/null +++ b/workstation/200_download-self-hosted.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# + +# Create getsentry folder and enter. +mkdir /home/user/getsentry +cd /home/user/getsentry + +# Pull down sentry and self-hosted. +git clone https://github.com/getsentry/sentry.git +git clone https://github.com/getsentry/self-hosted.git +cd self-hosted diff --git a/workstation/201_install-self-hosted.sh b/workstation/201_install-self-hosted.sh new file mode 100644 index 00000000000..31d36d4c3c6 --- /dev/null +++ b/workstation/201_install-self-hosted.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# + +# Install self-hosted. Assumed `200_download-self-hosted.sh` has already run. +./install.sh --skip-commit-check --skip-user-creation --skip-sse42-requirements --no-report-self-hosted-issues + +# Apply CSRF override to the newly installed sentry settings. +echo "CSRF_TRUSTED_ORIGINS = [\"https://9000-$web_host/"]" >>/home/user/getsentry/self-hosted/sentry/sentry.conf.py diff --git a/workstation/299_setup-completed.sh b/workstation/299_setup-completed.sh new file mode 100644 index 00000000000..a07e8637286 --- /dev/null +++ b/workstation/299_setup-completed.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# + +# Add a dot file to the home directory indicating that the setup has been completed successfully. +# The host-side of the connection will look for this file when polling for completion to indicate to +# the user that the workstation is ready for use. +# +# Works under the assumption that this is the last setup script to run! +echo "ready_at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" >/home/user/.sentry.workstation.remote diff --git a/workstation/README.md b/workstation/README.md new file mode 100644 index 00000000000..13a307e3db9 --- /dev/null +++ b/workstation/README.md @@ -0,0 +1,160 @@ +# Remote Self-Hosted Development on Google Cloud Workstation + +This document specifies how to set up remote workstation development for `self-hosted` using Google +Cloud. While this feature is primarily intended for Sentry developers seeking to develop or test +changes to `self-hosted`, in theory anyone with a Google Cloud account and the willingness to incur +the associated costs could replicate the setup described here. + +The goal of remote workstations is to provide turn-key instances for developing on `self-hosted`, in +either postinstall (the `/.install.sh` script has already run) or preinstall (it has not) modes. By +using Ubuntu as a base image, we are able to provide a fresh development environment that is very +similar to the Linux-based x86 instances that self-hosted is intended to be deployed to. + +Specifically, the goals of this effort are: + +- Create and manage turn-key virtual machines for development in either preinstall or postinstall + mode quickly and with minimal manual user input. +- Simulate real `self-hosted` deployment environments as faithfully as possible. +- Create a smooth developer experience when using VSCode and GitHub. + +The last point is worth emphasizing: this tool is specifically optimized to work well with VSCode +remote server (for the actual development) and GitHub (for pushing changes). Supporting any other +workflows is an explicit ***non-goal*** of this setup. + +The instructions here are for how to setup workstations as an administrator (that is, the person in +charge of managing and paying for the entire fleet of workstations). End users are expected to +create, connect to, manage, and shut down workstations as needed via the the `sentry` developer CLI +using the `sentry workstations ...` set of commands. For most use cases outside of +Sentry-the-company, the administrator and end user will be the same individual: they'll configure +their Google Cloud projects and billing, and then use them via `sentry workstations ...` commands on +their local machine. + +## Configuring Google Cloud + +You'll need to use two Google Cloud services to enable remote `self-hosted` deployment: Google Cloud +Workstations to run the actual virtual machines, and the Artifact Registry to store the base images +described in the adjacent Dockerfiles. + +The rest of this document will assume that you are configuring these services to be used from the +west coast of the United States (ie: `us-west1`), but a similar set of processes could be applied +for any region supported by Google Cloud. + +### Creating an Artifact Registry + +You can create an artifact registry using the Google Cloud Platform UI +[here](https://console.cloud.google.com/artifacts): + +![Create an Artifact Registry](./docs/img/create_artifcat_registry.png) + +The dialog should be straightforward. We'll name our new repository `sentry-workstation-us` and put +it in `us-west1`, but you could change these to whatever options suit your liking. Leave the +remaining configurations as they are: + +![Create a Repository](./docs/img/create_repository.png) + +### Setting up Cloud Workstations + +To use Google Cloud Workstations, you'll need to make at least one workstation cluster, and at least +one configuration therein. + +Navigate to the services [control panel](https://console.cloud.google.com/workstations/overview). +From here, you'll need to make one cluster for each region you plan to support. We'll make one for +`us-west1` in this tutorial, naming it `us-west` for clarity: + +![Create a Workstation cluster](./docs/img/create_cluster.png) + +Now, create a new configuration for that cluster. There are a few choices to make here: + +- Do you want it to be a preinstall (ie, `./install.sh` has not run) or postinstall (it has) + instance? +- Do you want to use a small, standard or large resource allocation? +- How aggressively do you want to auto-sleep and auto-shutdown instances? More aggressive setups + will save money, but be more annoying for end users. + +For this example, we'll make a `postinstall` instance with a `standard` resource allocation, but you +can of course change these as you wish. + +On the first panel, name the instance (we recommend using the convention +`[INSTALL_KIND]-[SIZE]-[CLUSTER_NAME]`, so this one `postinstall-standard-us-west`) and assign it to +the existing cluster: + +![Create a config](./docs/img/create_config_1.png) + +Next, pick a resource and cost saving configuration that makes sense for you. In our experience, an +E2 instance is plenty for most day-to-day development work. + +![Create a config](./docs/img/create_config_2.png) + +On the third panel, select `Custom container image`, then choose one of your `postinstall` images +(see below for how to generate these). Assign the default Compute Engine service account to it, then +choose to `Create a new empty persistent disk` for it. A balanced 10GB disk should be plenty for +shorter development stints: + +![Create a config](./docs/img/create_config_3.png) + +On the last screen, set the appropriate IAM policy to allow access to the new machine for your +users. You should be ready to go! + +## Creating and uploading an image artifact + +Each Cloud Workstation configuration you create will need to use a Docker image, the `Dockerfile`s +and scripts for which are found in this directory. There are two kinds of images: `preinstall` (ie, +`./install.sh` has not run) and `postinstall` (it has). To proceed, you'll need to install the +`gcloud` and `docker` CLI, then login to both and set your project as the default: + +```shell +$> export GCP_PROJECT_ID=my-gcp-project # Obviously, your project is likely to have another name. +$> gcloud auth application-default login +$> gcloud config set project $GCP_PROJECT_ID +$> gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://us-docker.pkg.dev +``` + +Next, you'll set some useful variables for this session (note: the code below assumes we are pushing +to the `sentry-workstation-us` repository defined above): + +```shell +$> export GROUP=sentry-workstation # Pick whatever name you like here. +$> export REGION=us # Name your regions as you see fit - these are not tied to GCP definitions. +$> export PHASE=pre # Use `pre` for preinstall, `post` for postinstall. +$> export REPO=${GROUP}-${REGION} +$> export IMAGE_TAG=${GROUP}/${PHASE}install:latest +$> export IMAGE_URL=us-docker.pkg.dev/${GCP_PROJECT_ID}/${REPO}/${GROUP}/${PHASE}install:latest +``` + +Now, build the docker image of your choosing: + +```shell +$> docker build -t ${IMAGE_TAG} -f ./${PHASE}install/Dockerfile . +$> docker image ls | grep "${GROUP}/${PHASE}install" +``` + +Finally, upload it to the Google Cloud Artifact Registry repository of interest: + +```shell +$> docker tag ${IMAGE_TAG} ${IMAGE_URL} +$> docker push ${IMAGE_URL} +``` + +## Creating and connecting to a workstation + +Once the Google Cloud services are configured and the docker images uploaded per the instructions +above, end users should be able to list the configurations available to them using `sentry +workstations config`: + +```shell +$> sentry workstations configs --project=$GCP_PROJECT_ID + +NAME CLUSTER REGION MACHINE TYPE +postinstall-standard-us-west us-west us-west1 e2-standard-4 +postinstall-large-us-west us-west us-west1 e2-standard-8 +preinstall-standard-us-west us-west us-west1 e2-standard-4 +preinstall-large-us-west us-west us-west1 e2-standard-8 +``` + +They will then be able to create a new workstation using the `sentry workstations create` command, +connect to an existing one using `sentry workstations connect`, and use similar `sentry workstations +...` commands to disconnect from and destroy workstations, as well as to check the status of their +active connections. The `create` and `connect` commands will provide further instructions in-band on +how to connect a their local VSCode to the remote server, use SSH to connect to the terminal +directly, add a GitHub access token, or access their running `self-hosted` instance via a web +browser. diff --git a/workstation/commands.sh b/workstation/commands.sh new file mode 100644 index 00000000000..647c8615a1d --- /dev/null +++ b/workstation/commands.sh @@ -0,0 +1,4 @@ +sudo wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor >packages.microsoft.gpg +sudo install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg +sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list' +sudo rm -f packages.microsoft.gpg diff --git a/workstation/docs/img/create_artifcat_registry.png b/workstation/docs/img/create_artifcat_registry.png new file mode 100644 index 0000000000000000000000000000000000000000..e15afc306225d25d07d1f049cd7ab5f681a059d7 GIT binary patch literal 59445 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%0AS#A9XNPNQN%uT zwS22~^%x(Eg++ioO{tCRLZ~XzWgwWEft$iB^U9Jo^m5U&3rY_q*B4$`hfD*o-%>qd zs}>&%2iX0u%ZwWUEcFNeAm0eL`R8)kF;WUIv+%8XI=(VtWrQAG?5*iD`ZKeZg~csL zVf?eJ3>9~KR<|1fd~x6w->8U=em?)?yJ+JVI+3_lAlX0hHl1%oVYWz-QK$QZ>Y{A{ z+v)DwFxBDsUHnd9+uTp>2#Qh8-(nj8ba_#WkQ~V<4Zc`4)M&Q!g@|tWLnYBaqZ@vt zbc1z*!-@RO_n0grCvabCts9QZL9OsWN-7ooRFm2*q0^S$v6~yuN4k!-?~AcP)lxf( z*EgBH%hMNr`g4H~mB&rPHu4fH`z-jjAKmq>?sA`;41raUBR!)3%3M!(59xNag+V-T zMDDfolZFrHf)V>jBNl!~Sb%=%aEp~{YRD;rm?Nc}+M*>VaLe=Ta-A+2v52DXWA|C| zA!t3@s2OO0BIyFgq`sDOFe+R2%9c)7Q!!`Cwq%*Mho`dt4j0**D3gnxHNRm`GFi0) zoS+F7xspars5Rq!C_PoK>b)>9S!b?0Ya>y@yRUAHx<>Tko)7U3L-<9rGnvh>8s6`r z><{e#q1Btt?u`jxEu}D3{S-)%JqAwgAPR{^sO1~$umM5S-UQ{2sI;tW=+|fc!n3O5 z1G1c34nU_XtC3UTZ5lj)z0Jv`y{N<*K`b$fYl_Lc*rOdG+=cRBuZ2sZeC8rofv}$F zV)1#7V)9BjE*gG<^OYj?O)eTA5ZPOQz;A~A@P=`2o9fz3@a)&^i}XKdc8FvJP<6AO z$3Q!6EA*__)O(7NRuEb~wn>?OGsrQiEr+u9Tv1&7ccHh|`h>-L=Ze93!NGB_ZnEr^ z7jIWGvBgBD{)z1n2anwJ(PMXzj_gx;l3VdQ6}tCcojYve?bi^wj%OJjm2;R0Qkp*}9@J*izqfOKJ(>%8300HRRfIQn#bazpgBad!PhvrDzGslXKVLXr!XQA*Z zaA61@IvQX!~1^BL*N

k>9SB#!zK;C96x_4r6V~c;*t<|E&E^Ly;Pi?`*AU@aHeX&FAlAOdSi8;XZqc!8Y z0hZgtk+fKsy%CSJ>j}Go7rEUNj%?Fiy8V`u36uJLY3%pP$+v#O==Z)gW`F!(|CM2BNW#3K#brgPdQyx_%;v8@v zB~N@$k8n;G1owIXvf7d@fWF!U{1G^r&ztO;R9Q|3i3Im|2`PWg>i`5Wh(tMf%zKu| z(v)F|XGzjhq$%amyekxDDfc%WD^%qX-#^~`R8GNrwg?=hsfQGg%57qmNcvKMyXGrv zp7Oo5+;o?t#OP=4Na%O~*E8TUc<{`28IVwrx>lg1p%2cy8@a5&#e@~T#o^rFqkkIsK74)*ET!`mMm}J4BIxF zPF%a=1WtGYXV}yY_TU^<|G!_>dI=)~LxRM!`(I6MB-b=H9@@&Jmil_7TLS|l6AOoc ef&+qaK+_;%L-hmJZED60K;Y@>=d#Wzp$PzHYe|&= literal 0 HcmV?d00001 diff --git a/workstation/docs/img/create_cluster.png b/workstation/docs/img/create_cluster.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e6f2a3bbd0c13cad39f4640a9d9dcda81b7696 GIT binary patch literal 63658 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 literal 0 HcmV?d00001 diff --git a/workstation/docs/img/create_config_2.png b/workstation/docs/img/create_config_2.png new file mode 100644 index 0000000000000000000000000000000000000000..5e932ad9681323baddda3d54b07f88c14e1eaa9f GIT binary patch literal 105490 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

q7 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:VP!WC%=gdA%4WY29v+zN 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�?aVOie$kH60(o4+T)nQD6Q`$ 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

$ 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 og7#H=aQLVR%) vscode.gpg \ + && install -D -o root -g root -m 644 vscode.gpg /etc/apt/keyrings/vscode.gpg \ + && sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/vscode.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list' \ + && rm -f vscode.gpg \ + && apt-get update \ + && apt-get install -y code \ + && apt-get install -y curl \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/gh.gpg \ + && chmod go+r /usr/share/keyrings/gh.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gh.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \ + && rm -f gh.gpg \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +# Reset the DEBIAN_FRONTEND environment variable. +ENV DEBIAN_FRONTEND= diff --git a/workstation/preinstall/Dockerfile b/workstation/preinstall/Dockerfile new file mode 100644 index 00000000000..dff60220f0d --- /dev/null +++ b/workstation/preinstall/Dockerfile @@ -0,0 +1,31 @@ +# Use a predefined image with no out-of-the-box IDE set up. +FROM us-west1-docker.pkg.dev/cloud-workstations-images/predefined/base:latest + +# Modifications to the `/home` directory must take place outside of the Dockerfile. Add a startup +# script to handle the cloning of the `sentry` and `self-hosted` repositories. +COPY 200_download-self-hosted.sh /etc/workstation-startup.d/ +COPY 299_setup-completed.sh /etc/workstation-startup.d/ + +RUN chmod -R +x /etc/workstation-startup.d + +# Avoid prompts from apt by setting it to non-interactive. +ENV DEBIAN_FRONTEND=noninteractive + +# Install VSCode and the GitHub CLI. +RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > vscode.gpg \ + && install -D -o root -g root -m 644 vscode.gpg /etc/apt/keyrings/vscode.gpg \ + && sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/vscode.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list' \ + && rm -f vscode.gpg \ + && apt-get update \ + && apt-get install -y code \ + && apt-get install -y curl \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/gh.gpg \ + && chmod go+r /usr/share/keyrings/gh.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/gh.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \ + && rm -f gh.gpg \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +# Reset the DEBIAN_FRONTEND environment variable. +ENV DEBIAN_FRONTEND= From 082903c84a0a22daebf159ea5088e993005d4a2e Mon Sep 17 00:00:00 2001 From: Matthew T <20070360+mdtro@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:45:47 -0500 Subject: [PATCH 020/287] Upgrade postgres to 14.11 (#2975) chore(deps): bump postgres to latest 14 alpine version --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0b26520a9ce..0736bdb63c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -132,7 +132,7 @@ services: postgres: <<: *restart_policy # Using the same postgres version as Sentry dev for consistency purposes - image: "postgres:14.5" + image: "postgres:14.11-alpine" healthcheck: <<: *healthcheck_defaults # Using default user "postgres" from sentry/sentry.conf.example.py or value of POSTGRES_USER if provided From 3ead5cf11d2197eda107ed4e86f32ab67cfc1e0b Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 19 Apr 2024 14:22:29 -0700 Subject: [PATCH 021/287] Bump docker compose version in CI (#2980) * only rerun tests on v2.0.1 * change from http error to request error * use 3 retries like before --- .github/workflows/test.yml | 11 ++++++++--- _integration-test/conftest.py | 8 ++++++-- _integration-test/test_backup.py | 6 +++++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5dc896fecb6..b264e451441 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,11 +60,11 @@ jobs: fail-fast: false matrix: customizations: ["disabled", "enabled"] - compose_version: ["v2.0.1", "v2.7.0"] + compose_version: ["v2.0.1", "v2.26.0"] include: - compose_version: "v2.0.1" compose_path: "/usr/local/lib/docker/cli-plugins" - - compose_version: "v2.7.0" + - compose_version: "v2.26.0" compose_path: "/usr/local/lib/docker/cli-plugins" env: COMPOSE_PROJECT_NAME: self-hosted-${{ strategy.job-index }} @@ -110,7 +110,12 @@ jobs: command: ./install.sh - name: Integration Test - run: pytest --cov --junitxml=junit.xml --reruns 3 _integration-test/ --customizations=${{ matrix.customizations }} + run: | + if [ "${{ matrix.compose_version }}" = "v2.0.1" ]; then + pytest --reruns 3 --cov --junitxml=junit.xml _integration-test/ --customizations=${{ matrix.customizations }} + else + pytest --cov --junitxml=junit.xml _integration-test/ --customizations=${{ matrix.customizations }} + fi - name: Inspect failure if: failure() diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py index a8a229ac77a..9d2a0fff236 100644 --- a/_integration-test/conftest.py +++ b/_integration-test/conftest.py @@ -18,11 +18,15 @@ def pytest_addoption(parser): @pytest.fixture(scope="session", autouse=True) def configure_self_hosted_environment(request): - subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) + subprocess.run( + ["docker", "compose", "--ansi", "never", "up", "-d"], + check=True, + capture_output=True, + ) for i in range(TIMEOUT_SECONDS): try: response = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) - except httpx.NetworkError: + except httpx.RequestError: time.sleep(1) else: if response.status_code == 200: diff --git a/_integration-test/test_backup.py b/_integration-test/test_backup.py index 8c482869e8d..41c099741a2 100644 --- a/_integration-test/test_backup.py +++ b/_integration-test/test_backup.py @@ -55,7 +55,11 @@ def test_import(setup_backup_restore_env_variables): ["docker", "compose", "--ansi", "never", "run", "web", "upgrade", "--noinput"], check=True, ) - subprocess.run(["docker", "compose", "--ansi", "never", "up", "-d"], check=True) + subprocess.run( + ["docker", "compose", "--ansi", "never", "up", "-d"], + check=True, + capture_output=True, + ) sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") subprocess.run( [ From 8b6ba0db54053a69203ad57679b250bf4e3e2d07 Mon Sep 17 00:00:00 2001 From: edwardgou-sentry <83961295+edwardgou-sentry@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:11:11 -0400 Subject: [PATCH 022/287] fix(performance): Add spans-first-ui flag to enable starfish/performance module views in ui (#2993) Adds spans-first-ui flag to enable displaying starfish/performance modules in ui --- sentry/sentry.conf.example.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 5247cca12db..40e2728569f 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -308,6 +308,7 @@ def get_internal_network(): "organizations:mobile-ttid-ttfd-contribution", "organizations:starfish-mobile-appstart", "organizations:standalone-span-ingestion", + "organizations:spans-first-ui", ) # starfish related flags } ) From ebf887c931b0c6c02906fb9d2bd01e17bb3f1967 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 23 Apr 2024 13:28:01 -0700 Subject: [PATCH 023/287] Tweak e2e test github action (#2987) --- .github/workflows/test.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b264e451441..fd1146aa16a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,18 +24,6 @@ jobs: with: path: self-hosted - - name: Get Compose - run: | - # Always remove `docker compose` support as that's the newer version - # and comes installed by default nowadays. - sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" - # Docker Compose v1 is installed here, remove it - sudo rm -f "/usr/local/bin/docker-compose" - sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" - sudo mkdir -p "/usr/local/lib/docker/cli-plugins" - sudo curl -L https://github.com/docker/compose/releases/download/v2.7.0/docker-compose-`uname -s`-`uname -m` -o "/usr/local/lib/docker/cli-plugins/docker-compose" - sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" - - name: End to end tests uses: getsentry/action-self-hosted-e2e-tests@main with: From 6c67717fc530145514198143007af1c7ff5b03a3 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Wed, 24 Apr 2024 08:59:42 -0700 Subject: [PATCH 024/287] Sampling: Run e2e tests every 5 minutes (#2994) run tests every 5 minutes --- .github/workflows/sample-test-runs.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/sample-test-runs.yml diff --git a/.github/workflows/sample-test-runs.yml b/.github/workflows/sample-test-runs.yml new file mode 100644 index 00000000000..a3c24e4f7c0 --- /dev/null +++ b/.github/workflows/sample-test-runs.yml @@ -0,0 +1,23 @@ +name: Test +on: + schedule: + # kick off a job every 5 minutes + - cron: "*/5 * * * *" +defaults: + run: + shell: bash +jobs: + e2e-test: + if: github.repository_owner == 'getsentry' + runs-on: ubuntu-22.04 + name: "Sentry self-hosted end-to-end tests" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: self-hosted + + - name: End to end tests + uses: getsentry/action-self-hosted-e2e-tests@main + with: + project_name: self-hosted From 6f91da5ea5fd930223e656bf2f238e2357a2eeb1 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 26 Apr 2024 11:27:58 -0700 Subject: [PATCH 025/287] Fix master test failures (#3000) * fix for tests? * fix tests on master --- _integration-test/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py index 9d2a0fff236..b36097d6056 100644 --- a/_integration-test/conftest.py +++ b/_integration-test/conftest.py @@ -40,7 +40,7 @@ def configure_self_hosted_environment(request): #!/bin/bash touch /created-by-enhance-image apt-get update -apt-get install -y gcc libsasl2-dev python-dev libldap2-dev libssl-dev +apt-get install -y gcc libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev """ with open("sentry/enhance-image.sh", "w") as script_file: @@ -52,7 +52,7 @@ def configure_self_hosted_environment(request): with open("sentry/requirements.txt", "w") as req_file: req_file.write("python-ldap\n") os.environ["MINIMIZE_DOWNTIME"] = "1" - subprocess.run(["./install.sh"], check=True) + subprocess.run(["./install.sh"], check=True, capture_output=True) # Create test user subprocess.run( [ From f84bb3d8e8e5f185dfb952b220a071efcbc8f070 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 26 Apr 2024 13:19:16 -0700 Subject: [PATCH 026/287] Revert "Sampling: Run e2e tests every 5 minutes" (#2999) Revert "Sampling: Run e2e tests every 5 minutes (#2994)" This reverts commit 6c67717fc530145514198143007af1c7ff5b03a3. --- .github/workflows/sample-test-runs.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/sample-test-runs.yml diff --git a/.github/workflows/sample-test-runs.yml b/.github/workflows/sample-test-runs.yml deleted file mode 100644 index a3c24e4f7c0..00000000000 --- a/.github/workflows/sample-test-runs.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Test -on: - schedule: - # kick off a job every 5 minutes - - cron: "*/5 * * * *" -defaults: - run: - shell: bash -jobs: - e2e-test: - if: github.repository_owner == 'getsentry' - runs-on: ubuntu-22.04 - name: "Sentry self-hosted end-to-end tests" - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - path: self-hosted - - - name: End to end tests - uses: getsentry/action-self-hosted-e2e-tests@main - with: - project_name: self-hosted From 3150263073614f76054c2fcc4fce4ff4cee483bd Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 29 Apr 2024 10:47:32 -0700 Subject: [PATCH 027/287] Edit test file name (#3002) edit file name --- .../custom-ca-roots/{test.py => custom-ca-roots-test.py} | 0 _integration-test/test_run.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename _integration-test/custom-ca-roots/{test.py => custom-ca-roots-test.py} (100%) diff --git a/_integration-test/custom-ca-roots/test.py b/_integration-test/custom-ca-roots/custom-ca-roots-test.py similarity index 100% rename from _integration-test/custom-ca-roots/test.py rename to _integration-test/custom-ca-roots/custom-ca-roots-test.py diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index 84bc027c2b3..2b5774832b9 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -313,7 +313,7 @@ def test_custom_certificate_authorities(): with open(fake_test_cert_path, "wb") as cert_file: cert_file.write(fake_test_cert.public_bytes(serialization.Encoding.PEM)) shutil.copyfile( - "_integration-test/custom-ca-roots/test.py", + "_integration-test/custom-ca-roots/custom-ca-roots-test.py", "sentry/test-custom-ca-roots.py", ) From 935a68382beb8eaa8de1e33041076073595557b9 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 30 Apr 2024 18:05:37 +0000 Subject: [PATCH 028/287] release: 24.4.2 --- .env | 10 +++++----- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..b9b1a87a522 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.4.2 +SNUBA_IMAGE=getsentry/snuba:24.4.2 +RELAY_IMAGE=getsentry/relay:24.4.2 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.4.2 +VROOM_IMAGE=getsentry/vroom:24.4.2 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index d3bcd69c13d..735a055ac5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 24.4.2 + +### Various fixes & improvements + +- Edit test file name (#3002) by @hubertdeng123 +- Revert "Sampling: Run e2e tests every 5 minutes" (#2999) by @hubertdeng123 +- Fix master test failures (#3000) by @hubertdeng123 +- Sampling: Run e2e tests every 5 minutes (#2994) by @hubertdeng123 +- Tweak e2e test github action (#2987) by @hubertdeng123 +- fix(performance): Add spans-first-ui flag to enable starfish/performance module views in ui (#2993) by @edwardgou-sentry +- Bump docker compose version in CI (#2980) by @hubertdeng123 +- Upgrade postgres to 14.11 (#2975) by @mdtro +- Add workstation configuration (#2968) by @azaslavsky + ## 24.4.1 ### Various fixes & improvements diff --git a/README.md b/README.md index 13f4f2b8934..233f9a8f6b7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.4.2 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From aa5a50a9c0c1ee915362938b10a8ac94b8592d4b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 30 Apr 2024 18:42:25 +0000 Subject: [PATCH 029/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index b9b1a87a522..8e54a3df2c9 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.4.2 -SNUBA_IMAGE=getsentry/snuba:24.4.2 -RELAY_IMAGE=getsentry/relay:24.4.2 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.4.2 -VROOM_IMAGE=getsentry/vroom:24.4.2 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index 233f9a8f6b7..13f4f2b8934 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.4.2 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 6db528d71afbe705b47c6100f9e161a7f2a57106 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 30 Apr 2024 12:32:22 -0700 Subject: [PATCH 030/287] Bump kafka and zookeeper versions (#2988) * bump kafka and zookeeper versions --- docker-compose.yml | 4 ++-- install.sh | 1 + install/update-docker-volume-permissions.sh | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 install/update-docker-volume-permissions.sh diff --git a/docker-compose.yml b/docker-compose.yml index 0736bdb63c0..c7457b3f9eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,7 +160,7 @@ services: target: /opt/sentry/ zookeeper: <<: *restart_policy - image: "confluentinc/cp-zookeeper:5.5.7" + image: "confluentinc/cp-zookeeper:7.6.1" environment: ZOOKEEPER_CLIENT_PORT: "2181" CONFLUENT_SUPPORT_METRICS_ENABLE: "false" @@ -184,7 +184,7 @@ services: depends_on: zookeeper: <<: *depends_on-healthy - image: "confluentinc/cp-kafka:5.5.7" + image: "confluentinc/cp-kafka:7.6.1" environment: KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092" diff --git a/install.sh b/install.sh index 78a96fece61..53c80cdeff5 100755 --- a/install.sh +++ b/install.sh @@ -21,6 +21,7 @@ source install/check-minimum-requirements.sh # Let's go! Start impacting things. source install/turn-things-off.sh +source install/update-docker-volume-permissions.sh source install/create-docker-volumes.sh source install/ensure-files-from-examples.sh source install/check-memcached-backend.sh diff --git a/install/update-docker-volume-permissions.sh b/install/update-docker-volume-permissions.sh new file mode 100644 index 00000000000..463e01773e0 --- /dev/null +++ b/install/update-docker-volume-permissions.sh @@ -0,0 +1,15 @@ +echo "${_group}Ensuring Kafka and Zookeeper volumes have correct permissions ..." + +# Only supporting platforms on linux x86 platforms and not apple silicon. I'm assuming that folks using apple silicon are doing it for dev purposes and it's difficult +# to change permissions of docker volumes since it is run in a VM. +if [[ "$DOCKER_PLATFORM" = "linux/amd64" && -n "$(docker volume ls -q -f name=sentry-zookeeper)" && -n "$(docker volume ls -q -f name=sentry-kafka)" ]]; then + zookeeper_data_dir="/var/lib/docker/volumes/sentry-zookeeper/_data" + kafka_data_dir="/var/lib/docker/volumes/sentry-kafka/_data" + zookeeper_log_data_dir="/var/lib/docker/volumes/sentry-self-hosted_sentry-zookeeper-log/_data" + chmod -R a+w $zookeeper_data_dir $kafka_data_dir $zookeeper_log_data_dir && returncode=$? || returncode=$? + if [[ $returncode == "1" ]]; then + echo "WARNING: Error when setting appropriate permissions for zookeeper, kafka, and zookeeper log docker volumes. This may corrupt your self-hosted install. See https://github.com/confluentinc/kafka-images/issues/127 for context on why this was added." + fi +fi + +echo "${_endgroup}" From 9e36d2f57a431a236f86a4fcd38125faf7721232 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 2 May 2024 13:04:34 -0700 Subject: [PATCH 031/287] Add upgrade test (#3012) * add upgrade test --- .github/workflows/test.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd1146aa16a..e2303cd97d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,43 @@ jobs: - name: Unit Tests run: ./unit-test.sh + upgrade-test: + if: github.repository_owner == 'getsentry' + runs-on: ubuntu-22.04 + name: "Sentry upgrade test" + env: + REPORT_SELF_HOSTED_ISSUES: 0 + steps: + - name: Get latest self-hosted release version + run: | + LATEST_TAG=$(curl -s https://api.github.com/repos/getsentry/self-hosted/releases/latest | jq -r '.tag_name') + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + + - name: Checkout latest release + uses: actions/checkout@v4 + with: + ref: ${{ env.LATEST_TAG }} + + - name: Get Compose + run: | + # Docker Compose v1 is installed here, remove it + sudo rm -f "/usr/local/bin/docker-compose" + sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" + sudo mkdir -p "/usr/local/lib/docker/cli-plugins" + sudo curl -L https://github.com/docker/compose/releases/download/v2.26.0/docker-compose-`uname -s`-`uname -m` -o "/usr/local/lib/docker/cli-plugins/docker-compose" + sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" + + - name: Install ${{ env.LATEST_TAG }} + run: ./install.sh + + - name: Checkout current ref + uses: actions/checkout@v4 + + - name: Install current ref + run: | + # Hacky way to get around permissioning issues in update-docker-volume-permissions.sh script + sudo -E ./install.sh + integration-test: if: github.repository_owner == 'getsentry' runs-on: ubuntu-22.04 From 23fa29d38b5d1160249b9f8011b95000b98ee8fc Mon Sep 17 00:00:00 2001 From: Alexander Tarasov Date: Mon, 6 May 2024 13:55:18 +0200 Subject: [PATCH 032/287] fix: use nginx realip module (#2977) * fix: use nginx realip module * use Docker default address pools --- nginx/nginx.conf | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 3f1e6d847fd..febbadb59bc 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -43,11 +43,24 @@ http { proxy_next_upstream error timeout invalid_header http_502 http_503 non_idempotent; proxy_next_upstream_tries 2; + # Docker default address pools + # https://github.com/moby/libnetwork/blob/3797618f9a38372e8107d8c06f6ae199e1133ae8/ipamutils/utils.go#L10-L22 + set_real_ip_from 172.17.0.0/16; + set_real_ip_from 172.18.0.0/16; + set_real_ip_from 172.19.0.0/16; + set_real_ip_from 172.20.0.0/14; + set_real_ip_from 172.24.0.0/14; + set_real_ip_from 172.28.0.0/14; + set_real_ip_from 192.168.0.0/16; + set_real_ip_from 10.0.0.0/8; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + # Remove the Connection header if the client sends it, # it could be "close" to close a keepalive connection proxy_set_header Connection ''; proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Request-Id $request_id; proxy_read_timeout 30s; From 67382fd2abc0357e6d39634c4051cae0240da9c2 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 6 May 2024 09:22:03 -0700 Subject: [PATCH 033/287] Upgrade clickhouse to 23.8 (#3009) * upgrade clickhouse --- clickhouse/config.xml | 8 ++------ docker-compose.yml | 2 +- install.sh | 3 +++ install/detect-platform.sh | 2 -- install/upgrade-clickhouse.sh | 27 +++++++++++++++++++++++++++ 5 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 install/upgrade-clickhouse.sh diff --git a/clickhouse/config.xml b/clickhouse/config.xml index d26bfbb3850..87984468953 100644 --- a/clickhouse/config.xml +++ b/clickhouse/config.xml @@ -1,10 +1,6 @@ - - - - + + warning true diff --git a/docker-compose.yml b/docker-compose.yml index c7457b3f9eb..1c6c2ad0ebc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -217,7 +217,7 @@ services: build: context: ./clickhouse args: - BASE_IMAGE: "${CLICKHOUSE_IMAGE:-}" + BASE_IMAGE: "altinity/clickhouse-server:23.8.11.29.altinitystable" ulimits: nofile: soft: 262144 diff --git a/install.sh b/install.sh index 53c80cdeff5..f418a29fc19 100755 --- a/install.sh +++ b/install.sh @@ -20,6 +20,9 @@ source install/check-latest-commit.sh source install/check-minimum-requirements.sh # Let's go! Start impacting things. +# Upgrading clickhouse needs to come first before turning things off, since we need the old clickhouse image +# in order to determine whether or not the clickhouse version needs to be upgraded. +source install/upgrade-clickhouse.sh source install/turn-things-off.sh source install/update-docker-volume-permissions.sh source install/create-docker-volumes.sh diff --git a/install/detect-platform.sh b/install/detect-platform.sh index 32ef5ea1cdf..7404008f41c 100644 --- a/install/detect-platform.sh +++ b/install/detect-platform.sh @@ -20,10 +20,8 @@ fi export DOCKER_ARCH=$(docker info --format '{{.Architecture}}') if [[ "$DOCKER_ARCH" = "x86_64" ]]; then export DOCKER_PLATFORM="linux/amd64" - export CLICKHOUSE_IMAGE="altinity/clickhouse-server:21.8.13.1.altinitystable" elif [[ "$DOCKER_ARCH" = "aarch64" ]]; then export DOCKER_PLATFORM="linux/arm64" - export CLICKHOUSE_IMAGE="altinity/clickhouse-server:21.8.12.29.altinitydev.arm" else echo "FAIL: Unsupported docker architecture $DOCKER_ARCH." exit 1 diff --git a/install/upgrade-clickhouse.sh b/install/upgrade-clickhouse.sh new file mode 100644 index 00000000000..b0a8027b342 --- /dev/null +++ b/install/upgrade-clickhouse.sh @@ -0,0 +1,27 @@ +echo "${_group}Upgrading Clickhouse ..." + +# First check to see if user is upgrading by checking for existing clickhouse volume +if [[ -n "$(docker volume ls -q --filter name=sentry-clickhouse)" ]]; then + # Start clickhouse if it is not already running + $dc up -d clickhouse + + # Wait for clickhouse + RETRIES=30 + until $dc ps clickhouse | grep 'healthy' || [ $RETRIES -eq 0 ]; do + echo "Waiting for clickhouse server, $((RETRIES--)) remaining attempts..." + sleep 1 + done + + # In order to get to 23.8, we need to first upgrade go from 21.8 -> 22.8 -> 23.3 -> 23.8 + version=$($dc exec clickhouse clickhouse-client -q 'SELECT version()') + if [[ "$version" == "21.8.13.1.altinitystable" || "$version" == "21.8.12.29.altinitydev.arm" ]]; then + $dc down clickhouse + $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:22.8.15.25.altinitystable clickhouse + $dc up -d clickhouse + $dc down clickhouse + $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:23.3.19.33.altinitystable clickhouse + else + echo "Detected clickhouse version $version. Skipping upgrades!" + fi +fi +echo "${_endgroup}" From 1c72fbe6120444ac232df0dae242cd5af40b38df Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 6 May 2024 15:38:15 -0700 Subject: [PATCH 034/287] Add clickhouse healthchecks to upgrade (#3024) add clickhouse healthchecks --- install/upgrade-clickhouse.sh | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/install/upgrade-clickhouse.sh b/install/upgrade-clickhouse.sh index b0a8027b342..e9472384009 100644 --- a/install/upgrade-clickhouse.sh +++ b/install/upgrade-clickhouse.sh @@ -1,16 +1,21 @@ echo "${_group}Upgrading Clickhouse ..." -# First check to see if user is upgrading by checking for existing clickhouse volume -if [[ -n "$(docker volume ls -q --filter name=sentry-clickhouse)" ]]; then - # Start clickhouse if it is not already running - $dc up -d clickhouse - +function wait_for_clickhouse() { # Wait for clickhouse RETRIES=30 until $dc ps clickhouse | grep 'healthy' || [ $RETRIES -eq 0 ]; do echo "Waiting for clickhouse server, $((RETRIES--)) remaining attempts..." sleep 1 done +} + +# First check to see if user is upgrading by checking for existing clickhouse volume +if [[ -n "$(docker volume ls -q --filter name=sentry-clickhouse)" ]]; then + # Start clickhouse if it is not already running + $dc up -d clickhouse + + # Wait for clickhouse + wait_for_clickhouse # In order to get to 23.8, we need to first upgrade go from 21.8 -> 22.8 -> 23.3 -> 23.8 version=$($dc exec clickhouse clickhouse-client -q 'SELECT version()') @@ -18,8 +23,11 @@ if [[ -n "$(docker volume ls -q --filter name=sentry-clickhouse)" ]]; then $dc down clickhouse $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:22.8.15.25.altinitystable clickhouse $dc up -d clickhouse + wait_for_clickhouse $dc down clickhouse $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:23.3.19.33.altinitystable clickhouse + $dc up -d clickhouse + wait_for_clickhouse else echo "Detected clickhouse version $version. Skipping upgrades!" fi From 40eed10dbbdce14d464d94b3871aae1f52e03be6 Mon Sep 17 00:00:00 2001 From: John Date: Fri, 10 May 2024 14:05:19 -0700 Subject: [PATCH 035/287] remove ref to skip writes (#3041) --- docker-compose.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1c6c2ad0ebc..9c4774e2242 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -258,41 +258,41 @@ services: # Kafka consumer responsible for feeding events into Clickhouse snuba-errors-consumer: <<: *snuba_defaults - command: rust-consumer --storage errors --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage errors --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset # Kafka consumer responsible for feeding outcomes into Clickhouse # Use --auto-offset-reset=earliest to recover up to 7 days of TSDB data # since we did not do a proper migration snuba-outcomes-consumer: <<: *snuba_defaults - command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset snuba-outcomes-billing-consumer: <<: *snuba_defaults - command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write --raw-events-topic outcomes-billing + command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --raw-events-topic outcomes-billing # Kafka consumer responsible for feeding transactions data into Clickhouse snuba-transactions-consumer: <<: *snuba_defaults - command: rust-consumer --storage transactions --consumer-group transactions_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage transactions --consumer-group transactions_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-replays-consumer: <<: *snuba_defaults - command: rust-consumer --storage replays --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage replays --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-issue-occurrence-consumer: <<: *snuba_defaults - command: rust-consumer --storage search_issues --consumer-group generic_events_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage search_issues --consumer-group generic_events_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-metrics-consumer: <<: *snuba_defaults - command: rust-consumer --storage metrics_raw --consumer-group snuba-metrics-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage metrics_raw --consumer-group snuba-metrics-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-group-attributes-consumer: <<: *snuba_defaults - command: rust-consumer --storage group_attributes --consumer-group snuba-group-attributes-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage group_attributes --consumer-group snuba-group-attributes-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-generic-metrics-distributions-consumer: <<: *snuba_defaults - command: rust-consumer --storage generic_metrics_distributions_raw --consumer-group snuba-gen-metrics-distributions-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage generic_metrics_distributions_raw --consumer-group snuba-gen-metrics-distributions-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-generic-metrics-sets-consumer: <<: *snuba_defaults - command: rust-consumer --storage generic_metrics_sets_raw --consumer-group snuba-gen-metrics-sets-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage generic_metrics_sets_raw --consumer-group snuba-gen-metrics-sets-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-generic-metrics-counters-consumer: <<: *snuba_defaults - command: rust-consumer --storage generic_metrics_counters_raw --consumer-group snuba-gen-metrics-counters-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage generic_metrics_counters_raw --consumer-group snuba-gen-metrics-counters-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-replacer: <<: *snuba_defaults command: replacer --storage errors --auto-offset-reset=latest --no-strict-offset-reset @@ -307,13 +307,13 @@ services: command: subscriptions-scheduler-executor --dataset metrics --entity metrics_sets --entity metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-metrics-subscriptions-consumers --followed-consumer-group=snuba-metrics-consumers --schedule-ttl=60 --stale-threshold-seconds=900 snuba-profiling-profiles-consumer: <<: *snuba_defaults - command: rust-consumer --storage profiles --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage profiles --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset snuba-profiling-functions-consumer: <<: *snuba_defaults - command: rust-consumer --storage functions_raw --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage functions_raw --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset snuba-spans-consumer: <<: *snuba_defaults - command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --no-skip-write + command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset symbolicator: <<: *restart_policy image: "$SYMBOLICATOR_IMAGE" From 9de4b70ece8b28d83d4b2e4a4b642ff14af52ae0 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 13 May 2024 09:50:28 -0700 Subject: [PATCH 036/287] fix: Make docker volume script respect compose project name (#3039) make the script a bit more robust in finding compose peojct name --- install/update-docker-volume-permissions.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/update-docker-volume-permissions.sh b/install/update-docker-volume-permissions.sh index 463e01773e0..98cafc237d2 100644 --- a/install/update-docker-volume-permissions.sh +++ b/install/update-docker-volume-permissions.sh @@ -5,7 +5,7 @@ echo "${_group}Ensuring Kafka and Zookeeper volumes have correct permissions ... if [[ "$DOCKER_PLATFORM" = "linux/amd64" && -n "$(docker volume ls -q -f name=sentry-zookeeper)" && -n "$(docker volume ls -q -f name=sentry-kafka)" ]]; then zookeeper_data_dir="/var/lib/docker/volumes/sentry-zookeeper/_data" kafka_data_dir="/var/lib/docker/volumes/sentry-kafka/_data" - zookeeper_log_data_dir="/var/lib/docker/volumes/sentry-self-hosted_sentry-zookeeper-log/_data" + zookeeper_log_data_dir="/var/lib/docker/volumes/${COMPOSE_PROJECT_NAME}_sentry-zookeeper-log/_data" chmod -R a+w $zookeeper_data_dir $kafka_data_dir $zookeeper_log_data_dir && returncode=$? || returncode=$? if [[ $returncode == "1" ]]; then echo "WARNING: Error when setting appropriate permissions for zookeeper, kafka, and zookeeper log docker volumes. This may corrupt your self-hosted install. See https://github.com/confluentinc/kafka-images/issues/127 for context on why this was added." From 2b26c7ca78e4721cd74bc62f03093d77f421c7ee Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 16 May 2024 18:51:31 +0000 Subject: [PATCH 037/287] release: 24.5.0 --- .env | 10 +++++----- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..c43fada1445 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.5.0 +SNUBA_IMAGE=getsentry/snuba:24.5.0 +RELAY_IMAGE=getsentry/relay:24.5.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.5.0 +VROOM_IMAGE=getsentry/vroom:24.5.0 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index 735a055ac5a..ceab2dc54d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 24.5.0 + +### Various fixes & improvements + +- fix: Make docker volume script respect compose project name (#3039) by @hubertdeng123 +- remove ref to skip writes (#3041) by @john-z-yang +- Add clickhouse healthchecks to upgrade (#3024) by @hubertdeng123 +- Upgrade clickhouse to 23.8 (#3009) by @hubertdeng123 +- fix: use nginx realip module (#2977) by @oioki +- Add upgrade test (#3012) by @hubertdeng123 +- Bump kafka and zookeeper versions (#2988) by @hubertdeng123 + ## 24.4.2 ### Various fixes & improvements diff --git a/README.md b/README.md index 13f4f2b8934..74d84ae21c6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.5.0 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From f22bc2acf546d3732ce896a6f87ad7d379ba6b1f Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 16 May 2024 19:23:21 +0000 Subject: [PATCH 038/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index c43fada1445..8e54a3df2c9 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.5.0 -SNUBA_IMAGE=getsentry/snuba:24.5.0 -RELAY_IMAGE=getsentry/relay:24.5.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.5.0 -VROOM_IMAGE=getsentry/vroom:24.5.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index 74d84ae21c6..13f4f2b8934 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.5.0 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 9afc35910e66d80855815be0309b9e8fc1aa2974 Mon Sep 17 00:00:00 2001 From: Vova Luchaninov Date: Tue, 21 May 2024 19:30:11 +0200 Subject: [PATCH 039/287] Typo in config.example.yml (#3063) Update config.example.yml typo --- sentry/config.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/config.example.yml b/sentry/config.example.yml index 10f5c8c16f7..de0b68b99f8 100644 --- a/sentry/config.example.yml +++ b/sentry/config.example.yml @@ -117,7 +117,7 @@ transaction-events.force-disable-internal-project: true # slack.client-id: <'client id'> # slack.client-secret: # slack.signing-secret: -## If legacy-app is True use verfication-token instead of signing-secret +## If legacy-app is True use verification-token instead of signing-secret # slack.verification-token: From 6032d980254bafc8858acd53ede53669747f9d00 Mon Sep 17 00:00:00 2001 From: Nicolas Boutet Date: Wed, 22 May 2024 04:14:52 +0200 Subject: [PATCH 040/287] Fix install: use dynamic docker root dir instead of hardcoded one (#3064) Fix: use dynamic docker root dir instead of hardcoded --- install/update-docker-volume-permissions.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/install/update-docker-volume-permissions.sh b/install/update-docker-volume-permissions.sh index 98cafc237d2..5dbc04f69f0 100644 --- a/install/update-docker-volume-permissions.sh +++ b/install/update-docker-volume-permissions.sh @@ -3,9 +3,10 @@ echo "${_group}Ensuring Kafka and Zookeeper volumes have correct permissions ... # Only supporting platforms on linux x86 platforms and not apple silicon. I'm assuming that folks using apple silicon are doing it for dev purposes and it's difficult # to change permissions of docker volumes since it is run in a VM. if [[ "$DOCKER_PLATFORM" = "linux/amd64" && -n "$(docker volume ls -q -f name=sentry-zookeeper)" && -n "$(docker volume ls -q -f name=sentry-kafka)" ]]; then - zookeeper_data_dir="/var/lib/docker/volumes/sentry-zookeeper/_data" - kafka_data_dir="/var/lib/docker/volumes/sentry-kafka/_data" - zookeeper_log_data_dir="/var/lib/docker/volumes/${COMPOSE_PROJECT_NAME}_sentry-zookeeper-log/_data" + docker_root_dir=$(docker info --format '{{.DockerRootDir}}') + zookeeper_data_dir="${docker_root_dir}/volumes/sentry-zookeeper/_data" + kafka_data_dir="${docker_root_dir}/volumes/sentry-kafka/_data" + zookeeper_log_data_dir="${docker_root_dir}/volumes/${COMPOSE_PROJECT_NAME}_sentry-zookeeper-log/_data" chmod -R a+w $zookeeper_data_dir $kafka_data_dir $zookeeper_log_data_dir && returncode=$? || returncode=$? if [[ $returncode == "1" ]]; then echo "WARNING: Error when setting appropriate permissions for zookeeper, kafka, and zookeeper log docker volumes. This may corrupt your self-hosted install. See https://github.com/confluentinc/kafka-images/issues/127 for context on why this was added." From 6917e398bb603e1673b2e2dc7ca2ccbb466305c7 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 28 May 2024 13:47:01 -0700 Subject: [PATCH 041/287] chore: Add comment explaining the one liner in clickhouse config (#3085) add comment explaining the one liner in clickhouse config --- clickhouse/config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clickhouse/config.xml b/clickhouse/config.xml index 87984468953..19a0ecf80a9 100644 --- a/clickhouse/config.xml +++ b/clickhouse/config.xml @@ -1,5 +1,5 @@ - + warning From ede1e6f83613f1506e656f8015c844f889ca574a Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Wed, 29 May 2024 13:32:30 -0400 Subject: [PATCH 042/287] ref(spans): Add new feature flags needed (#3092) * ref(spans): Add new feature flags needed * Add new UI flags * Remove addons ingest flag as we're not releasing addons yet * Remove obsolete flags --- sentry/sentry.conf.example.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 40e2728569f..5213d1e06af 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -294,21 +294,22 @@ def get_internal_network(): "projects:servicehooks", ) + ( - "projects:span-metrics-extraction", + "organizations:deprecate-fid-from-performance-score", + "organizations:indexed-spans-extraction", + "organizations:insights-entry-points", + "organizations:insights-initial-modules", + "organizations:mobile-ttid-ttfd-contribution", + "organizations:performance-calculate-score-relay", + "organizations:spans-first-ui", + "organizations:standalone-span-ingestion", "organizations:starfish-browser-resource-module-image-view", "organizations:starfish-browser-resource-module-ui", "organizations:starfish-browser-webvitals", "organizations:starfish-browser-webvitals-pageoverview-v2", - "organizations:starfish-browser-webvitals-use-backend-scores", - "organizations:performance-calculate-score-relay", "organizations:starfish-browser-webvitals-replace-fid-with-inp", - "organizations:deprecate-fid-from-performance-score", - "organizations:performance-database-view", - "organizations:performance-screens-view", - "organizations:mobile-ttid-ttfd-contribution", + "organizations:starfish-browser-webvitals-use-backend-scores", "organizations:starfish-mobile-appstart", - "organizations:standalone-span-ingestion", - "organizations:spans-first-ui", + "projects:span-metrics-extraction", ) # starfish related flags } ) From 0dabb5a4cc6dabc3e238db60c5d1afca4afcea41 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Wed, 29 May 2024 11:30:44 -0700 Subject: [PATCH 043/287] Different approach to editing permissions of docker volumes (#3084) * different approach to editing permissions of docker volumes --- .github/workflows/test.yml | 4 +--- install/update-docker-volume-permissions.sh | 11 ++--------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2303cd97d3..8acdb988133 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -73,9 +73,7 @@ jobs: uses: actions/checkout@v4 - name: Install current ref - run: | - # Hacky way to get around permissioning issues in update-docker-volume-permissions.sh script - sudo -E ./install.sh + run: ./install.sh integration-test: if: github.repository_owner == 'getsentry' diff --git a/install/update-docker-volume-permissions.sh b/install/update-docker-volume-permissions.sh index 5dbc04f69f0..8ac0be400c4 100644 --- a/install/update-docker-volume-permissions.sh +++ b/install/update-docker-volume-permissions.sh @@ -2,15 +2,8 @@ echo "${_group}Ensuring Kafka and Zookeeper volumes have correct permissions ... # Only supporting platforms on linux x86 platforms and not apple silicon. I'm assuming that folks using apple silicon are doing it for dev purposes and it's difficult # to change permissions of docker volumes since it is run in a VM. -if [[ "$DOCKER_PLATFORM" = "linux/amd64" && -n "$(docker volume ls -q -f name=sentry-zookeeper)" && -n "$(docker volume ls -q -f name=sentry-kafka)" ]]; then - docker_root_dir=$(docker info --format '{{.DockerRootDir}}') - zookeeper_data_dir="${docker_root_dir}/volumes/sentry-zookeeper/_data" - kafka_data_dir="${docker_root_dir}/volumes/sentry-kafka/_data" - zookeeper_log_data_dir="${docker_root_dir}/volumes/${COMPOSE_PROJECT_NAME}_sentry-zookeeper-log/_data" - chmod -R a+w $zookeeper_data_dir $kafka_data_dir $zookeeper_log_data_dir && returncode=$? || returncode=$? - if [[ $returncode == "1" ]]; then - echo "WARNING: Error when setting appropriate permissions for zookeeper, kafka, and zookeeper log docker volumes. This may corrupt your self-hosted install. See https://github.com/confluentinc/kafka-images/issues/127 for context on why this was added." - fi +if [[ -n "$(docker volume ls -q -f name=sentry-zookeeper)" && -n "$(docker volume ls -q -f name=sentry-kafka)" ]]; then + docker run --rm -v "sentry-zookeeper:/sentry-zookeeper-data" -v "sentry-kafka:/sentry-kafka-data" -v "${COMPOSE_PROJECT_NAME}_sentry-zookeeper-log:/sentry-zookeeper-log-data" busybox chmod -R a+w /sentry-zookeeper-data /sentry-kafka-data /sentry-zookeeper-log-data fi echo "${_endgroup}" From c40b1530d104e9481d172e06dc6810bea0795d5c Mon Sep 17 00:00:00 2001 From: Jann Kleen Date: Wed, 29 May 2024 23:59:22 +0200 Subject: [PATCH 044/287] Update minimum docker compose requirement (#3078) * Update minimum docker compose requirement docker compose down is now required. * Update docker compose version to new minimum in CI config. --- .github/workflows/test.yml | 6 +++--- README.md | 2 +- install/_min-requirements.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8acdb988133..e565392c897 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,9 +83,9 @@ jobs: fail-fast: false matrix: customizations: ["disabled", "enabled"] - compose_version: ["v2.0.1", "v2.26.0"] + compose_version: ["v2.19.0", "v2.26.0"] include: - - compose_version: "v2.0.1" + - compose_version: "v2.19.0" compose_path: "/usr/local/lib/docker/cli-plugins" - compose_version: "v2.26.0" compose_path: "/usr/local/lib/docker/cli-plugins" @@ -134,7 +134,7 @@ jobs: - name: Integration Test run: | - if [ "${{ matrix.compose_version }}" = "v2.0.1" ]; then + if [ "${{ matrix.compose_version }}" = "v2.19.0" ]; then pytest --reruns 3 --cov --junitxml=junit.xml _integration-test/ --customizations=${{ matrix.customizations }} else pytest --cov --junitxml=junit.xml _integration-test/ --customizations=${{ matrix.customizations }} diff --git a/README.md b/README.md index 13f4f2b8934..7315466c250 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docke ## Requirements * Docker 19.03.6+ -* Compose 2.0.1+ +* Compose 2.19.0+ * 4 CPU Cores * 16 GB RAM * 20 GB Free Disk Space diff --git a/install/_min-requirements.sh b/install/_min-requirements.sh index e756c765d80..f8836db0863 100644 --- a/install/_min-requirements.sh +++ b/install/_min-requirements.sh @@ -1,6 +1,6 @@ # Don't forget to update the README and othes docs when you change these! MIN_DOCKER_VERSION='19.03.6' -MIN_COMPOSE_VERSION='2.0.1' +MIN_COMPOSE_VERSION='2.19.0' MIN_RAM_HARD=3800 # MB MIN_RAM_SOFT=7800 # MB MIN_CPU_HARD=2 From f8e95ec8686f23a4ed14d413599d3169e5beb165 Mon Sep 17 00:00:00 2001 From: Dan Fuller Date: Tue, 4 Jun 2024 10:35:30 -0700 Subject: [PATCH 045/287] feat: Add crons task consumers (#3106) We now process tasks via Kafka consumers instead of celerybeat. This needs to be added to self-hosted as well --- docker-compose.yml | 6 ++++++ install/create-kafka-topics.sh | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9c4774e2242..84fb56da793 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -384,6 +384,12 @@ services: ingest-monitors: <<: *sentry_defaults command: run consumer --no-strict-offset-reset ingest-monitors --consumer-group ingest-monitors + monitors-clock-tick: + <<: *sentry_defaults + command: run consumer --no-strict-offset-reset monitors-clock-tick --consumer-group monitors-clock-tick + monitors-clock-tasks: + <<: *sentry_defaults + command: run consumer --no-strict-offset-reset monitors-clock-tasks --consumer-group monitors-clock-tasks post-process-forwarder-errors: <<: *sentry_defaults command: run consumer post-process-forwarder-errors --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-commit-log --synchronize-commit-group=snuba-consumers diff --git a/install/create-kafka-topics.sh b/install/create-kafka-topics.sh index 63e0cffa016..ff2ba8e6608 100644 --- a/install/create-kafka-topics.sh +++ b/install/create-kafka-topics.sh @@ -14,7 +14,7 @@ done # XXX(BYK): We cannot use auto.create.topics as Confluence and Apache hates it now (and makes it very hard to enable) EXISTING_KAFKA_TOPICS=$($dc exec -T kafka kafka-topics --list --bootstrap-server kafka:9092 2>/dev/null) -NEEDED_KAFKA_TOPICS="ingest-attachments ingest-transactions ingest-events ingest-replay-recordings profiles ingest-occurrences ingest-metrics ingest-performance-metrics ingest-monitors" +NEEDED_KAFKA_TOPICS="ingest-attachments ingest-transactions ingest-events ingest-replay-recordings profiles ingest-occurrences ingest-metrics ingest-performance-metrics ingest-monitors monitors-clock-tasks" for topic in $NEEDED_KAFKA_TOPICS; do if ! echo "$EXISTING_KAFKA_TOPICS" | grep -qE "(^| )$topic( |$)"; then $dc exec kafka kafka-topics --create --topic $topic --bootstrap-server kafka:9092 @@ -22,4 +22,11 @@ for topic in $NEEDED_KAFKA_TOPICS; do fi done +# This topic must have only a single partition for the consumer to work correctly +# https://github.com/getsentry/ops/blob/7dbc26f39c584ec924c8fef2ad5c532d6a158be3/k8s/clusters/us/_topicctl.yaml#L288-L295 + +if ! echo "$EXISTING_KAFKA_TOPICS" | grep -qE "(^| )monitors-clock-tick( |$)"; then + $dc exec kafka kafka-topics --create --topic monitors-clock-tick --bootstrap-server kafka:9092 --partitions 1 +fi + echo "${_endgroup}" From 3455a33a4ce1f2e80c25adb64d8845f45ae62196 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 4 Jun 2024 12:07:35 -0700 Subject: [PATCH 046/287] Update consumer flags (#3112) update consumer flags --- docker-compose.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 84fb56da793..61d87ae32ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -380,25 +380,25 @@ services: command: run consumer ingest-occurrences --consumer-group ingest-occurrences ingest-profiles: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset ingest-profiles --consumer-group ingest-profiles + command: run consumer ingest-profiles --consumer-group ingest-profiles ingest-monitors: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset ingest-monitors --consumer-group ingest-monitors + command: run consumer ingest-monitors --consumer-group ingest-monitors monitors-clock-tick: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset monitors-clock-tick --consumer-group monitors-clock-tick + command: run consumer monitors-clock-tick --consumer-group monitors-clock-tick monitors-clock-tasks: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset monitors-clock-tasks --consumer-group monitors-clock-tasks + command: run consumer monitors-clock-tasks --consumer-group monitors-clock-tasks post-process-forwarder-errors: <<: *sentry_defaults - command: run consumer post-process-forwarder-errors --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-commit-log --synchronize-commit-group=snuba-consumers + command: run consumer --no-strict-offset-reset post-process-forwarder-errors --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-commit-log --synchronize-commit-group=snuba-consumers post-process-forwarder-transactions: <<: *sentry_defaults - command: run consumer post-process-forwarder-transactions --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-transactions-commit-log --synchronize-commit-group transactions_group + command: run consumer --no-strict-offset-reset post-process-forwarder-transactions --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-transactions-commit-log --synchronize-commit-group transactions_group post-process-forwarder-issue-platform: <<: *sentry_defaults - command: run consumer post-process-forwarder-issue-platform --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-generic-events-commit-log --synchronize-commit-group generic_events_group + command: run consumer --no-strict-offset-reset post-process-forwarder-issue-platform --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-generic-events-commit-log --synchronize-commit-group generic_events_group subscription-consumer-events: <<: *sentry_defaults command: run consumer events-subscription-results --consumer-group query-subscription-consumer From 601897160706c8544d09420a231593c47eb73487 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 4 Jun 2024 21:37:10 +0000 Subject: [PATCH 047/287] release: 24.5.1 --- .env | 10 +++++----- CHANGELOG.md | 13 +++++++++++++ README.md | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..a8dcb6c9252 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.5.1 +SNUBA_IMAGE=getsentry/snuba:24.5.1 +RELAY_IMAGE=getsentry/relay:24.5.1 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.5.1 +VROOM_IMAGE=getsentry/vroom:24.5.1 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index ceab2dc54d1..59b9aeb2595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 24.5.1 + +### Various fixes & improvements + +- Update consumer flags (#3112) by @hubertdeng123 +- feat: Add crons task consumers (#3106) by @wedamija +- Update minimum docker compose requirement (#3078) by @JannKleen +- Different approach to editing permissions of docker volumes (#3084) by @hubertdeng123 +- ref(spans): Add new feature flags needed (#3092) by @phacops +- chore: Add comment explaining the one liner in clickhouse config (#3085) by @hubertdeng123 +- Fix install: use dynamic docker root dir instead of hardcoded one (#3064) by @boutetnico +- Typo in config.example.yml (#3063) by @luchaninov + ## 24.5.0 ### Various fixes & improvements diff --git a/README.md b/README.md index 7315466c250..3dddde17a91 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.5.1 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From b819d95c169192ac606b086cfd4fc534055091df Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 4 Jun 2024 22:00:05 +0000 Subject: [PATCH 048/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index a8dcb6c9252..8e54a3df2c9 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.5.1 -SNUBA_IMAGE=getsentry/snuba:24.5.1 -RELAY_IMAGE=getsentry/relay:24.5.1 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.5.1 -VROOM_IMAGE=getsentry/vroom:24.5.1 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index 3dddde17a91..7315466c250 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.5.1 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From e8146adafd2a3d5a206c6d70633f4277d1c6cf1b Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 5 Jun 2024 20:42:47 +0200 Subject: [PATCH 049/287] Bump Python SDK version used in tests (#3108) Bump Python SDK version in tests --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ef178da75a..42bf67b2472 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -sentry-sdk>=1.39.2 +sentry-sdk>=2.4.0 pytest>=8.0.0 pytest-cov>=4.1.0 pytest-rerunfailures>=11.0 From 419d6ccf4e4d0dde2db301e43370d019f9a4c69b Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 7 Jun 2024 09:49:58 -0700 Subject: [PATCH 050/287] Use non-alpine postgres (#3116) use non-alpine postgres --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 61d87ae32ef..f88d74d99e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -132,7 +132,7 @@ services: postgres: <<: *restart_policy # Using the same postgres version as Sentry dev for consistency purposes - image: "postgres:14.11-alpine" + image: "postgres:14.11" healthcheck: <<: *healthcheck_defaults # Using default user "postgres" from sentry/sentry.conf.example.py or value of POSTGRES_USER if provided From a6cb07691018bbe66360208b5f5f507f64b3f606 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Wed, 12 Jun 2024 15:37:15 -0700 Subject: [PATCH 051/287] Use general kafka topic creation in self-hosted (#3121) * use general kafka topic creation --- install.sh | 1 - install/create-kafka-topics.sh | 32 -------------------------- install/set-up-and-migrate-database.sh | 4 ++-- 3 files changed, 2 insertions(+), 35 deletions(-) delete mode 100644 install/create-kafka-topics.sh diff --git a/install.sh b/install.sh index f418a29fc19..ac5a2f0f4b2 100755 --- a/install.sh +++ b/install.sh @@ -34,7 +34,6 @@ source install/update-docker-images.sh source install/build-docker-images.sh source install/install-wal2json.sh source install/bootstrap-snuba.sh -source install/create-kafka-topics.sh source install/upgrade-postgres.sh source install/set-up-and-migrate-database.sh source install/geoip.sh diff --git a/install/create-kafka-topics.sh b/install/create-kafka-topics.sh deleted file mode 100644 index ff2ba8e6608..00000000000 --- a/install/create-kafka-topics.sh +++ /dev/null @@ -1,32 +0,0 @@ -echo "${_group}Creating additional Kafka topics ..." - -$dc up -d --no-build --no-recreate kafka - -while [ true ]; do - kafka_healthy=$($dc ps kafka | grep 'healthy') - if [ ! -z "$kafka_healthy" ]; then - break - fi - - echo "Kafka container is not healthy, waiting for 30 seconds. If this took too long, abort the installation process, and check your Kafka configuration" - sleep 30s -done - -# XXX(BYK): We cannot use auto.create.topics as Confluence and Apache hates it now (and makes it very hard to enable) -EXISTING_KAFKA_TOPICS=$($dc exec -T kafka kafka-topics --list --bootstrap-server kafka:9092 2>/dev/null) -NEEDED_KAFKA_TOPICS="ingest-attachments ingest-transactions ingest-events ingest-replay-recordings profiles ingest-occurrences ingest-metrics ingest-performance-metrics ingest-monitors monitors-clock-tasks" -for topic in $NEEDED_KAFKA_TOPICS; do - if ! echo "$EXISTING_KAFKA_TOPICS" | grep -qE "(^| )$topic( |$)"; then - $dc exec kafka kafka-topics --create --topic $topic --bootstrap-server kafka:9092 - echo "" - fi -done - -# This topic must have only a single partition for the consumer to work correctly -# https://github.com/getsentry/ops/blob/7dbc26f39c584ec924c8fef2ad5c532d6a158be3/k8s/clusters/us/_topicctl.yaml#L288-L295 - -if ! echo "$EXISTING_KAFKA_TOPICS" | grep -qE "(^| )monitors-clock-tick( |$)"; then - $dc exec kafka kafka-topics --create --topic monitors-clock-tick --bootstrap-server kafka:9092 --partitions 1 -fi - -echo "${_endgroup}" diff --git a/install/set-up-and-migrate-database.sh b/install/set-up-and-migrate-database.sh index 6880622ae6c..09c87dc8450 100644 --- a/install/set-up-and-migrate-database.sh +++ b/install/set-up-and-migrate-database.sh @@ -19,7 +19,7 @@ with connection.cursor() as cursor: " if [[ -n "${CI:-}" || "${SKIP_USER_CREATION:-0}" == 1 ]]; then - $dcr web upgrade --noinput + $dcr web upgrade --noinput --create-kafka-topics echo "" echo "Did not prompt for user creation. Run the following command to create one" echo "yourself (recommended):" @@ -27,7 +27,7 @@ if [[ -n "${CI:-}" || "${SKIP_USER_CREATION:-0}" == 1 ]]; then echo " $dc_base run --rm web createuser" echo "" else - $dcr web upgrade + $dcr web upgrade --create-kafka-topics fi echo "${_endgroup}" From 7ec463082bdc957470a98bffb2f1b685d5aba36c Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sat, 15 Jun 2024 18:05:28 +0000 Subject: [PATCH 052/287] release: 24.6.0 --- .env | 10 +++++----- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..c718928d762 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.6.0 +SNUBA_IMAGE=getsentry/snuba:24.6.0 +RELAY_IMAGE=getsentry/relay:24.6.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.6.0 +VROOM_IMAGE=getsentry/vroom:24.6.0 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b9aeb2595..2d077215e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 24.6.0 + +### Various fixes & improvements + +- Use general kafka topic creation in self-hosted (#3121) by @hubertdeng123 +- Use non-alpine postgres (#3116) by @hubertdeng123 +- Bump Python SDK version used in tests (#3108) by @sentrivana + ## 24.5.1 ### Various fixes & improvements diff --git a/README.md b/README.md index 7315466c250..b8ccfeced36 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.6.0 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 05fa62a0c3029216cdf6a7c391df0fbca3070a7a Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 18 Jun 2024 19:34:29 +0000 Subject: [PATCH 053/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index c718928d762..8e54a3df2c9 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.6.0 -SNUBA_IMAGE=getsentry/snuba:24.6.0 -RELAY_IMAGE=getsentry/relay:24.6.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.6.0 -VROOM_IMAGE=getsentry/vroom:24.6.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index b8ccfeced36..7315466c250 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.6.0 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From da06c0f230454b91fada3980ba2e1e012cedf1d2 Mon Sep 17 00:00:00 2001 From: Riccardo Busetti Date: Thu, 20 Jun 2024 14:56:57 +0200 Subject: [PATCH 054/287] feat(relay): Forward /api/0/relays/* to inner relays (#3144) --- nginx/nginx.conf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index febbadb59bc..c24fe7ec188 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -85,6 +85,9 @@ http { location ~ ^/api/[1-9]\d*/ { proxy_pass http://relay; } + location ^~ /api/0/relays/ { + proxy_pass http://relay; + } location / { proxy_pass http://sentry; } From e39ac04ccc278606284cdac66e1e5fe0453b57a3 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 9 Jul 2024 07:37:50 +0700 Subject: [PATCH 055/287] feat: add insights feature flags (#3152) * feat: add insights feature flags * feat: add span metrics extractions addons feature flag --- sentry/sentry.conf.example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 5213d1e06af..38a461f46cb 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -298,9 +298,9 @@ def get_internal_network(): "organizations:indexed-spans-extraction", "organizations:insights-entry-points", "organizations:insights-initial-modules", + "organizations:insights-addon-modules", "organizations:mobile-ttid-ttfd-contribution", "organizations:performance-calculate-score-relay", - "organizations:spans-first-ui", "organizations:standalone-span-ingestion", "organizations:starfish-browser-resource-module-image-view", "organizations:starfish-browser-resource-module-ui", @@ -310,6 +310,7 @@ def get_internal_network(): "organizations:starfish-browser-webvitals-use-backend-scores", "organizations:starfish-mobile-appstart", "projects:span-metrics-extraction", + "projects:span-metrics-extraction-addons", ) # starfish related flags } ) From 65779a77a535e57c1034c04b8de49f1363ac35a2 Mon Sep 17 00:00:00 2001 From: Christoph Keller Date: Tue, 9 Jul 2024 21:09:01 +0200 Subject: [PATCH 056/287] Update sentry-admin.sh to select its own working directory (#3184) `sentry-admin.sh` only works when called from the working directory and not using its absolute path. This change makes it also callable using its absolute path. --- sentry-admin.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry-admin.sh b/sentry-admin.sh index 85705516d27..d162c82114d 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Set the script directory as working directory. +cd $(dirname $0) + # Detect docker and platform state. source install/dc-detect-version.sh source install/detect-platform.sh From c18da05a4641a1c0c45eb51486b7418ab3c20376 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 12 Jul 2024 09:33:49 -0700 Subject: [PATCH 057/287] Check postgres os before proceeding with install (#3197) * check postgres os before proceeding * use dc --- install/set-up-and-migrate-database.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install/set-up-and-migrate-database.sh b/install/set-up-and-migrate-database.sh index 09c87dc8450..7bf74f40e95 100644 --- a/install/set-up-and-migrate-database.sh +++ b/install/set-up-and-migrate-database.sh @@ -9,6 +9,12 @@ until $dc exec postgres psql -U postgres -c "select 1" >/dev/null 2>&1 || [ $RET sleep 1 done +os=$($dc exec postgres cat /etc/os-release | grep 'ID=debian') +if [[ -z $os ]]; then + echo "Postgres image debian check failed, exiting..." + exit 1 +fi + # Using django ORM to provide broader support for users with external databases $dcr web shell -c " from django.db import connection From 8936d2a27e3226bf609a76dbd0f0722786badef6 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 15 Jul 2024 18:05:40 +0000 Subject: [PATCH 058/287] release: 24.7.0 --- .env | 10 +++++----- CHANGELOG.md | 9 +++++++++ README.md | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..029fa6f13f2 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.7.0 +SNUBA_IMAGE=getsentry/snuba:24.7.0 +RELAY_IMAGE=getsentry/relay:24.7.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.7.0 +VROOM_IMAGE=getsentry/vroom:24.7.0 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d077215e49..9490a34b78f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 24.7.0 + +### Various fixes & improvements + +- Check postgres os before proceeding with install (#3197) by @hubertdeng123 +- Update sentry-admin.sh to select its own working directory (#3184) by @theoriginalgri +- feat: add insights feature flags (#3152) by @aldy505 +- feat(relay): Forward /api/0/relays/* to inner relays (#3144) by @iambriccardo + ## 24.6.0 ### Various fixes & improvements diff --git a/README.md b/README.md index 7315466c250..b5d626f6b6d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.7.0 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From f04ee1a11a1efa69774495da6bda7d15e9a4395a Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 15 Jul 2024 23:39:34 +0000 Subject: [PATCH 059/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 029fa6f13f2..8e54a3df2c9 100644 --- a/.env +++ b/.env @@ -5,11 +5,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.7.0 -SNUBA_IMAGE=getsentry/snuba:24.7.0 -RELAY_IMAGE=getsentry/relay:24.7.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.7.0 -VROOM_IMAGE=getsentry/vroom:24.7.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index b5d626f6b6d..7315466c250 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.7.0 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From ca5f35c94f33d50be04c35bcb4c16cc705538df2 Mon Sep 17 00:00:00 2001 From: Riya Chakraborty <47572810+ayirr7@users.noreply.github.com> Date: Tue, 16 Jul 2024 09:59:57 -0700 Subject: [PATCH 060/287] feat(generic-metrics): Add gauges to docker compose, re-try (#3177) * add gauges * use rust consumer instead --- docker-compose.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f88d74d99e7..e524d0fd0e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -177,8 +177,7 @@ services: - "sentry-secrets:/etc/zookeeper/secrets" healthcheck: <<: *healthcheck_defaults - test: - ["CMD-SHELL", 'echo "ruok" | nc -w 2 localhost 2181 | grep imok'] + test: ["CMD-SHELL", 'echo "ruok" | nc -w 2 localhost 2181 | grep imok'] kafka: <<: *restart_policy depends_on: @@ -250,7 +249,8 @@ services: # Override the entrypoint in order to avoid using envvars for config. # Futz with settings so we can keep mmdb and conf in same dir on host # (image looks for them in separate dirs by default). - entrypoint: ["/usr/bin/geoipupdate", "-d", "/sentry", "-f", "/sentry/GeoIP.conf"] + entrypoint: + ["/usr/bin/geoipupdate", "-d", "/sentry", "-f", "/sentry/GeoIP.conf"] volumes: - "./geoip:/sentry" snuba-api: @@ -293,6 +293,9 @@ services: snuba-generic-metrics-counters-consumer: <<: *snuba_defaults command: rust-consumer --storage generic_metrics_counters_raw --consumer-group snuba-gen-metrics-counters-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + snuba-generic-metrics-gauges-consumer: + <<: *snuba_defaults + command: rust-consumer --storage generic_metrics_gauges_raw --consumer-group snuba-gen-metrics-gauges-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset snuba-replacer: <<: *snuba_defaults command: replacer --storage errors --auto-offset-reset=latest --no-strict-offset-reset From 485d3ffd2f6e6bc5b64afb0721ac2e75ed5ee33a Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 22 Jul 2024 10:23:05 -0700 Subject: [PATCH 061/287] Add errors only self-hosted infrastructure (#3190) --- .env | 1 + docker-compose.yml | 123 ++++++++++++++++++++++++---------- sentry/sentry.conf.example.py | 3 + 3 files changed, 91 insertions(+), 36 deletions(-) diff --git a/.env b/.env index 8e54a3df2c9..430cdac1d5d 100644 --- a/.env +++ b/.env @@ -1,4 +1,5 @@ COMPOSE_PROJECT_NAME=sentry-self-hosted +COMPOSE_PROFILES=feature-complete SENTRY_EVENT_RETENTION_DAYS=90 # You can either use a port number or an IP:PORT combo for SENTRY_BIND # See https://docs.docker.com/compose/compose-file/#ports for more diff --git a/docker-compose.yml b/docker-compose.yml index e524d0fd0e8..af7aab1d2de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,24 +36,8 @@ x-sentry-defaults: &sentry_defaults <<: *depends_on-default snuba-api: <<: *depends_on-default - snuba-errors-consumer: - <<: *depends_on-default - snuba-outcomes-consumer: - <<: *depends_on-default - snuba-outcomes-billing-consumer: - <<: *depends_on-default - snuba-transactions-consumer: - <<: *depends_on-default - snuba-subscription-consumer-events: - <<: *depends_on-default - snuba-subscription-consumer-transactions: - <<: *depends_on-default - snuba-replacer: - <<: *depends_on-default symbolicator: <<: *depends_on-default - vroom: - <<: *depends_on-default entrypoint: "/etc/sentry/entrypoint.sh" command: ["run", "web"] environment: @@ -71,6 +55,7 @@ x-sentry-defaults: &sentry_defaults GRPC_DEFAULT_SSL_ROOTS_FILE_PATH_ENV_VAR: *ca_bundle # Leaving the value empty to just pass whatever is set # on the host system (or in the .env file) + COMPOSE_PROFILES: SENTRY_EVENT_RETENTION_DAYS: SENTRY_MAIL_HOST: SENTRY_MAX_EXTERNAL_SOURCEMAP_SIZE: @@ -268,55 +253,84 @@ services: snuba-outcomes-billing-consumer: <<: *snuba_defaults command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --raw-events-topic outcomes-billing + snuba-group-attributes-consumer: + <<: *snuba_defaults + command: rust-consumer --storage group_attributes --consumer-group snuba-group-attributes-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + snuba-replacer: + <<: *snuba_defaults + command: replacer --storage errors --auto-offset-reset=latest --no-strict-offset-reset + snuba-subscription-consumer-events: + <<: *snuba_defaults + command: subscriptions-scheduler-executor --dataset events --entity events --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-events-subscriptions-consumers --followed-consumer-group=snuba-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + ############################################# + ## Feature Complete Sentry Snuba Consumers ## + ############################################# # Kafka consumer responsible for feeding transactions data into Clickhouse snuba-transactions-consumer: <<: *snuba_defaults command: rust-consumer --storage transactions --consumer-group transactions_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + profiles: + - feature-complete snuba-replays-consumer: <<: *snuba_defaults command: rust-consumer --storage replays --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + profiles: + - feature-complete snuba-issue-occurrence-consumer: <<: *snuba_defaults command: rust-consumer --storage search_issues --consumer-group generic_events_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + profiles: + - feature-complete snuba-metrics-consumer: <<: *snuba_defaults command: rust-consumer --storage metrics_raw --consumer-group snuba-metrics-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset - snuba-group-attributes-consumer: + profiles: + - feature-complete + snuba-subscription-consumer-transactions: <<: *snuba_defaults - command: rust-consumer --storage group_attributes --consumer-group snuba-group-attributes-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: subscriptions-scheduler-executor --dataset transactions --entity transactions --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-transactions-subscriptions-consumers --followed-consumer-group=transactions_group --schedule-ttl=60 --stale-threshold-seconds=900 + profiles: + - feature-complete + snuba-subscription-consumer-metrics: + <<: *snuba_defaults + command: subscriptions-scheduler-executor --dataset metrics --entity metrics_sets --entity metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-metrics-subscriptions-consumers --followed-consumer-group=snuba-metrics-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + profiles: + - feature-complete snuba-generic-metrics-distributions-consumer: <<: *snuba_defaults command: rust-consumer --storage generic_metrics_distributions_raw --consumer-group snuba-gen-metrics-distributions-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + profiles: + - feature-complete snuba-generic-metrics-sets-consumer: <<: *snuba_defaults command: rust-consumer --storage generic_metrics_sets_raw --consumer-group snuba-gen-metrics-sets-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + profiles: + - feature-complete snuba-generic-metrics-counters-consumer: <<: *snuba_defaults command: rust-consumer --storage generic_metrics_counters_raw --consumer-group snuba-gen-metrics-counters-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + profiles: + - feature-complete snuba-generic-metrics-gauges-consumer: <<: *snuba_defaults command: rust-consumer --storage generic_metrics_gauges_raw --consumer-group snuba-gen-metrics-gauges-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset - snuba-replacer: - <<: *snuba_defaults - command: replacer --storage errors --auto-offset-reset=latest --no-strict-offset-reset - snuba-subscription-consumer-events: - <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset events --entity events --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-events-subscriptions-consumers --followed-consumer-group=snuba-consumers --schedule-ttl=60 --stale-threshold-seconds=900 - snuba-subscription-consumer-transactions: - <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset transactions --entity transactions --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-transactions-subscriptions-consumers --followed-consumer-group=transactions_group --schedule-ttl=60 --stale-threshold-seconds=900 - snuba-subscription-consumer-metrics: - <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset metrics --entity metrics_sets --entity metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-metrics-subscriptions-consumers --followed-consumer-group=snuba-metrics-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + profiles: + - feature-complete snuba-profiling-profiles-consumer: <<: *snuba_defaults command: rust-consumer --storage profiles --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + profiles: + - feature-complete snuba-profiling-functions-consumer: <<: *snuba_defaults command: rust-consumer --storage functions_raw --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + profiles: + - feature-complete snuba-spans-consumer: <<: *snuba_defaults command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + profiles: + - feature-complete symbolicator: <<: *restart_policy image: "$SYMBOLICATOR_IMAGE" @@ -363,57 +377,90 @@ services: attachments-consumer: <<: *sentry_defaults command: run consumer ingest-attachments --consumer-group ingest-consumer + post-process-forwarder-errors: + <<: *sentry_defaults + command: run consumer --no-strict-offset-reset post-process-forwarder-errors --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-commit-log --synchronize-commit-group=snuba-consumers + subscription-consumer-events: + <<: *sentry_defaults + command: run consumer events-subscription-results --consumer-group query-subscription-consumer + ############################################## + ## Feature Complete Sentry Ingest Consumers ## + ############################################## transactions-consumer: <<: *sentry_defaults command: run consumer ingest-transactions --consumer-group ingest-consumer + profiles: + - feature-complete metrics-consumer: <<: *sentry_defaults command: run consumer ingest-metrics --consumer-group metrics-consumer + profiles: + - feature-complete generic-metrics-consumer: <<: *sentry_defaults command: run consumer ingest-generic-metrics --consumer-group generic-metrics-consumer + profiles: + - feature-complete billing-metrics-consumer: <<: *sentry_defaults command: run consumer billing-metrics-consumer --consumer-group billing-metrics-consumer + profiles: + - feature-complete ingest-replay-recordings: <<: *sentry_defaults command: run consumer ingest-replay-recordings --consumer-group ingest-replay-recordings + profiles: + - feature-complete ingest-occurrences: <<: *sentry_defaults command: run consumer ingest-occurrences --consumer-group ingest-occurrences + profiles: + - feature-complete ingest-profiles: <<: *sentry_defaults command: run consumer ingest-profiles --consumer-group ingest-profiles + profiles: + - feature-complete ingest-monitors: <<: *sentry_defaults command: run consumer ingest-monitors --consumer-group ingest-monitors + profiles: + - feature-complete monitors-clock-tick: <<: *sentry_defaults command: run consumer monitors-clock-tick --consumer-group monitors-clock-tick + profiles: + - feature-complete monitors-clock-tasks: <<: *sentry_defaults command: run consumer monitors-clock-tasks --consumer-group monitors-clock-tasks - post-process-forwarder-errors: - <<: *sentry_defaults - command: run consumer --no-strict-offset-reset post-process-forwarder-errors --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-commit-log --synchronize-commit-group=snuba-consumers + profiles: + - feature-complete post-process-forwarder-transactions: <<: *sentry_defaults command: run consumer --no-strict-offset-reset post-process-forwarder-transactions --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-transactions-commit-log --synchronize-commit-group transactions_group + profiles: + - feature-complete post-process-forwarder-issue-platform: <<: *sentry_defaults command: run consumer --no-strict-offset-reset post-process-forwarder-issue-platform --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-generic-events-commit-log --synchronize-commit-group generic_events_group - subscription-consumer-events: - <<: *sentry_defaults - command: run consumer events-subscription-results --consumer-group query-subscription-consumer + profiles: + - feature-complete subscription-consumer-transactions: <<: *sentry_defaults command: run consumer transactions-subscription-results --consumer-group query-subscription-consumer + profiles: + - feature-complete subscription-consumer-metrics: <<: *sentry_defaults command: run consumer metrics-subscription-results --consumer-group query-subscription-consumer + profiles: + - feature-complete subscription-consumer-generic-metrics: <<: *sentry_defaults command: run consumer generic-metrics-subscription-results --consumer-group query-subscription-consumer + profiles: + - feature-complete sentry-cleanup: <<: *sentry_defaults image: sentry-cleanup-self-hosted-local @@ -469,6 +516,8 @@ services: depends_on: kafka: <<: *depends_on-healthy + profiles: + - feature-complete vroom-cleanup: <<: *restart_policy image: vroom-cleanup-self-hosted-local @@ -484,6 +533,8 @@ services: command: '"0 0 * * * find /var/lib/sentry-profiles -type f -mtime +$SENTRY_EVENT_RETENTION_DAYS -delete"' volumes: - sentry-vroom:/var/lib/sentry-profiles + profiles: + - feature-complete volumes: # These store application data that should persist across restarts. diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 38a461f46cb..0f8d611700d 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -363,3 +363,6 @@ def get_internal_network(): # https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS # CSRF_TRUSTED_ORIGINS = ["https://example.com/", "http://127.0.0.1:9000/"] + +# If you would like to use self-hosted Sentry with only errors enabled, please set this +SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") == "feature-complete" From fd2f9fa74ca906bb48b7d51fb5fa14d8bc7e1fc6 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 23 Jul 2024 11:14:55 -0700 Subject: [PATCH 062/287] Fix: errors only config flag (#3220) fix bug with errors only flag --- sentry/sentry.conf.example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 0f8d611700d..144d94b6be2 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -365,4 +365,4 @@ def get_internal_network(): # CSRF_TRUSTED_ORIGINS = ["https://example.com/", "http://127.0.0.1:9000/"] # If you would like to use self-hosted Sentry with only errors enabled, please set this -SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") == "feature-complete" +SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete" From f330f3040d3ab81055f92b4ac4265fbbfe17487c Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 23 Jul 2024 19:39:00 +0000 Subject: [PATCH 063/287] release: 24.7.1 --- .env | 10 +++++----- CHANGELOG.md | 8 ++++++++ README.md | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 430cdac1d5d..d663c61b1ec 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.7.1 +SNUBA_IMAGE=getsentry/snuba:24.7.1 +RELAY_IMAGE=getsentry/relay:24.7.1 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.7.1 +VROOM_IMAGE=getsentry/vroom:24.7.1 WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index 9490a34b78f..8925d795cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 24.7.1 + +### Various fixes & improvements + +- Fix: errors only config flag (#3220) by @hubertdeng123 +- Add errors only self-hosted infrastructure (#3190) by @hubertdeng123 +- feat(generic-metrics): Add gauges to docker compose, re-try (#3177) by @ayirr7 + ## 24.7.0 ### Various fixes & improvements diff --git a/README.md b/README.md index 7315466c250..2ce9171dde7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.7.1 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 22605fd745efcad217293ff3d041d146de699156 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 23 Jul 2024 20:04:25 +0000 Subject: [PATCH 064/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index d663c61b1ec..430cdac1d5d 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.7.1 -SNUBA_IMAGE=getsentry/snuba:24.7.1 -RELAY_IMAGE=getsentry/relay:24.7.1 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.7.1 -VROOM_IMAGE=getsentry/vroom:24.7.1 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/README.md b/README.md index 2ce9171dde7..7315466c250 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.7.1 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From cd7c460e15f7986bf206c3ea4c7e6cafa607604f Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Tue, 30 Jul 2024 22:29:14 +0200 Subject: [PATCH 065/287] Use CDN by default for JS SDK Loader (#3213) Co-authored-by: Hubert Deng --- sentry/sentry.conf.example.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 144d94b6be2..d39be661dbc 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -364,5 +364,12 @@ def get_internal_network(): # CSRF_TRUSTED_ORIGINS = ["https://example.com/", "http://127.0.0.1:9000/"] +################# +# JS SDK Loader # +################# + +JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" + + # If you would like to use self-hosted Sentry with only errors enabled, please set this SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete" From 0b11564e3652607baab6ec614f777517057ddfee Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 2 Aug 2024 02:01:17 +0700 Subject: [PATCH 066/287] feat: enable user feedback feature (#3193) * feat: enable user feedback feature --- docker-compose.yml | 5 +++++ sentry/sentry.conf.example.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index af7aab1d2de..ce67c1a72e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -426,6 +426,11 @@ services: command: run consumer ingest-monitors --consumer-group ingest-monitors profiles: - feature-complete + ingest-feedback-events: + <<: *sentry_defaults + command: run consumer ingest-feedback-events --consumer-group ingest-feedback + profiles: + - feature-complete monitors-clock-tick: <<: *sentry_defaults command: run consumer monitors-clock-tick --consumer-group monitors-clock-tick diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index d39be661dbc..a200124f833 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -293,6 +293,7 @@ def get_internal_network(): "projects:rate-limits", "projects:servicehooks", ) + # Starfish related flags + ( "organizations:deprecate-fid-from-performance-score", "organizations:indexed-spans-extraction", @@ -311,10 +312,23 @@ def get_internal_network(): "organizations:starfish-mobile-appstart", "projects:span-metrics-extraction", "projects:span-metrics-extraction-addons", - ) # starfish related flags + ) + # User Feedback related flags + + ( + "organizations:user-feedback-ingest", + "organizations:user-feedback-replay-clip", + "organizations:user-feedback-ui", + "organizations:feedback-visible", + "organizations:feedback-ingest", + "organizations:feedback-post-process-group", + ) } ) +# Temporary flag to mark User Feedback to be produced to the dedicated feedback topic by relay. +# This will be removed at a later time after it's considered stable and fully rolled out. +SENTRY_OPTIONS["feedback.ingest-topic.rollout-rate"] = 1.0 + ####################### # MaxMind Integration # ####################### From 20af97258a41e0ca87af532eb614180accc94eb0 Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Fri, 2 Aug 2024 17:07:42 -0400 Subject: [PATCH 067/287] remove python-dev (#3242) this package does not exist on modern debians, the headers are already available in the docker image anyway resolves #3226 --- sentry/enhance-image.example.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/enhance-image.example.sh b/sentry/enhance-image.example.sh index e5b31826462..3f43756dd73 100755 --- a/sentry/enhance-image.example.sh +++ b/sentry/enhance-image.example.sh @@ -3,5 +3,5 @@ # Enhance the base $SENTRY_IMAGE with additional dependencies, plugins - see https://github.com/getsentry/self-hosted#enhance-sentry-image # For example: # apt-get update -# apt-get install -y gcc libsasl2-dev python-dev libldap2-dev libssl-dev +# apt-get install -y gcc libsasl2-dev libldap2-dev libssl-dev # pip install python-ldap From 053f4010b61adf4677d8ac81488d41dd95461fba Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:15:51 -0400 Subject: [PATCH 068/287] add `-euo pipefail` to enhance-image.example.sh (#3246) --- sentry/enhance-image.example.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/enhance-image.example.sh b/sentry/enhance-image.example.sh index 3f43756dd73..c3ae96c96da 100755 --- a/sentry/enhance-image.example.sh +++ b/sentry/enhance-image.example.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -euo pipefail # Enhance the base $SENTRY_IMAGE with additional dependencies, plugins - see https://github.com/getsentry/self-hosted#enhance-sentry-image # For example: From 534a874c0bb8808d16001aa2d230e098993e82a5 Mon Sep 17 00:00:00 2001 From: Michal Kuffa Date: Mon, 12 Aug 2024 18:12:37 +0200 Subject: [PATCH 069/287] Remove cdc and wal2json and use the default postgres entrypoint (#3260) * Remove cdc and wal2json and use the default postgres entrypoint * Remove the last bits of wal2json install * Remove read-only postgres volume bind --- .env | 1 - .gitignore | 3 --- docker-compose.yml | 11 -------- install.sh | 1 - install/install-wal2json.sh | 45 -------------------------------- postgres/init_hba.sh | 7 ----- postgres/postgres-entrypoint.sh | 46 --------------------------------- scripts/bump-version.sh | 4 --- 8 files changed, 118 deletions(-) delete mode 100644 install/install-wal2json.sh delete mode 100755 postgres/init_hba.sh delete mode 100755 postgres/postgres-entrypoint.sh diff --git a/.env b/.env index 430cdac1d5d..db8bdb8dba2 100644 --- a/.env +++ b/.env @@ -11,7 +11,6 @@ SNUBA_IMAGE=getsentry/snuba:nightly RELAY_IMAGE=getsentry/relay:nightly SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly VROOM_IMAGE=getsentry/vroom:nightly -WAL2JSON_VERSION=latest HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/.gitignore b/.gitignore index 7311a09ae9c..13f8982c056 100644 --- a/.gitignore +++ b/.gitignore @@ -98,9 +98,6 @@ geoip/GeoIP.conf geoip/*.mmdb geoip/.geoipupdate.lock -# wal2json download -postgres/wal2json - # integration testing _integration-test/custom-ca-roots/nginx/* sentry/test-custom-ca-roots.py diff --git a/docker-compose.yml b/docker-compose.yml index ce67c1a72e2..654220d4ded 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -126,23 +126,12 @@ services: [ "postgres", "-c", - "wal_level=logical", - "-c", - "max_replication_slots=1", - "-c", - "max_wal_senders=1", - "-c", "max_connections=${POSTGRES_MAX_CONNECTIONS:-100}", ] environment: POSTGRES_HOST_AUTH_METHOD: "trust" - entrypoint: /opt/sentry/postgres-entrypoint.sh volumes: - "sentry-postgres:/var/lib/postgresql/data" - - type: bind - read_only: true - source: ./postgres/ - target: /opt/sentry/ zookeeper: <<: *restart_policy image: "confluentinc/cp-zookeeper:7.6.1" diff --git a/install.sh b/install.sh index ac5a2f0f4b2..ee2e008d1e7 100755 --- a/install.sh +++ b/install.sh @@ -32,7 +32,6 @@ source install/ensure-relay-credentials.sh source install/generate-secret-key.sh source install/update-docker-images.sh source install/build-docker-images.sh -source install/install-wal2json.sh source install/bootstrap-snuba.sh source install/upgrade-postgres.sh source install/set-up-and-migrate-database.sh diff --git a/install/install-wal2json.sh b/install/install-wal2json.sh deleted file mode 100644 index 9df0df7603e..00000000000 --- a/install/install-wal2json.sh +++ /dev/null @@ -1,45 +0,0 @@ -echo "${_group}Downloading and installing wal2json ..." - -WAL2JSON_DIR=postgres/wal2json -FILE_TO_USE="$WAL2JSON_DIR/wal2json.so" -ARCH=$(uname -m) -FILE_NAME="wal2json-Linux-$ARCH-glibc.so" - -docker_curl() { - # The environment variables can be specified in lower case or upper case. - # The lower case version has precedence. http_proxy is an exception as it is only available in lower case. - docker run --rm -e http_proxy -e https_proxy -e HTTPS_PROXY -e no_proxy -e NO_PROXY curlimages/curl:7.77.0 \ - --connect-timeout 5 \ - --max-time 10 \ - --retry 5 \ - --retry-max-time 60 \ - "$@" -} - -if [[ $WAL2JSON_VERSION == "latest" ]]; then - # Hard-code this. Super-hacky. We were curling the GitHub API here but - # hitting rate limits in CI. This library hasn't seen a new release for a - # year and a half at time of writing. - # - # If you're reading this do us a favor and go check: - # - # https://github.com/getsentry/wal2json/releases - # - # If there's a new release can you update this please? If not maybe subscribe - # for notifications on the repo with "Watch > Custom > Releases". Together we - # can make a difference. - VERSION=0.0.2 -else - VERSION=$WAL2JSON_VERSION -fi - -mkdir -p "$WAL2JSON_DIR" -if [ ! -f "$WAL2JSON_DIR/$VERSION/$FILE_NAME" ]; then - mkdir -p "$WAL2JSON_DIR/$VERSION" - docker_curl -L \ - "https://github.com/getsentry/wal2json/releases/download/$VERSION/$FILE_NAME" \ - >"$WAL2JSON_DIR/$VERSION/$FILE_NAME" -fi -cp "$WAL2JSON_DIR/$VERSION/$FILE_NAME" "$FILE_TO_USE" - -echo "${_endgroup}" diff --git a/postgres/init_hba.sh b/postgres/init_hba.sh deleted file mode 100755 index 9952ab14864..00000000000 --- a/postgres/init_hba.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Initializes the pg_hba file with access permissions to the replication -# slots. - -set -e - -{ echo "host replication all all trust"; } >>"$PGDATA/pg_hba.conf" diff --git a/postgres/postgres-entrypoint.sh b/postgres/postgres-entrypoint.sh deleted file mode 100755 index 68a469f7cae..00000000000 --- a/postgres/postgres-entrypoint.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# This script replaces the default docker entrypoint for postgres in the -# development environment. -# Its job is to ensure postgres is properly configured to support the -# Change Data Capture pipeline (by setting access permissions and installing -# the replication plugin we use for CDC). Unfortunately the default -# Postgres image does not allow this level of configurability so we need -# to do it this way in order not to have to publish and maintain our own -# Postgres image. -# -# This then, at the end, transfers control to the default entrypoint. - -set -e - -prep_init_db() { - cp /opt/sentry/init_hba.sh /docker-entrypoint-initdb.d/init_hba.sh -} - -cdc_setup_hba_conf() { - # Ensure pg-hba is properly configured to allow connections - # to the replication slots. - - PG_HBA="$PGDATA/pg_hba.conf" - if [ ! -f "$PG_HBA" ]; then - echo "DB not initialized. Postgres will take care of pg_hba" - elif [ "$(grep -c -E "^host\s+replication" "$PGDATA"/pg_hba.conf)" != 0 ]; then - echo "Replication config already present in pg_hba. Not changing anything." - else - # Execute the same script we run on DB initialization - /opt/sentry/init_hba.sh - fi -} - -bind_wal2json() { - # Copy the file in the right place - cp /opt/sentry/wal2json/wal2json.so $(pg_config --pkglibdir)/wal2json.so -} - -echo "Setting up Change Data Capture" - -prep_init_db -if [ "$1" = 'postgres' ]; then - cdc_setup_hba_conf - bind_wal2json -fi -exec /usr/local/bin/docker-entrypoint.sh "$@" diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index e59a1d1eb27..808df09aa17 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -4,11 +4,7 @@ set -eu OLD_VERSION="$1" NEW_VERSION="$2" -WAL2JSON_VERSION=${WAL2JSON_VERSION:-$(curl -s "https://api.github.com/repos/getsentry/wal2json/releases/latest" | grep -Po '"tag_name": "\K.*?(?=")')} - -sed -i -e "s/^WAL2JSON_VERSION=\([^:]\+\):.\+\$/WAL2JSON_VERSION=\1:$WAL2JSON_VERSION/" .env sed -i -e "s/^\(SENTRY\|SNUBA\|RELAY\|SYMBOLICATOR\|VROOM\)_IMAGE=\([^:]\+\):.\+\$/\1_IMAGE=\2:$NEW_VERSION/" .env sed -i -e "s/^\# Self-Hosted Sentry .*/# Self-Hosted Sentry $NEW_VERSION/" README.md echo "New version: $NEW_VERSION" -echo "New wal2json version: $WAL2JSON_VERSION" From d64f72fafaf5937636df29fd108d471c63671aeb Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:05:26 -0700 Subject: [PATCH 070/287] ref(feedback): cleanup topic rollout option (#3256) --- sentry/sentry.conf.example.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index a200124f833..f20c4efdcb6 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -325,10 +325,6 @@ def get_internal_network(): } ) -# Temporary flag to mark User Feedback to be produced to the dedicated feedback topic by relay. -# This will be removed at a later time after it's considered stable and fully rolled out. -SENTRY_OPTIONS["feedback.ingest-topic.rollout-rate"] = 1.0 - ####################### # MaxMind Integration # ####################### From 9b815ac58d202f84904dc40eaad3aac689644f79 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:55:04 -0700 Subject: [PATCH 071/287] Revert "ref(feedback): cleanup topic rollout option" (#3262) Revert "ref(feedback): cleanup topic rollout option (#3256)" This reverts commit d64f72fafaf5937636df29fd108d471c63671aeb. --- sentry/sentry.conf.example.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index f20c4efdcb6..a200124f833 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -325,6 +325,10 @@ def get_internal_network(): } ) +# Temporary flag to mark User Feedback to be produced to the dedicated feedback topic by relay. +# This will be removed at a later time after it's considered stable and fully rolled out. +SENTRY_OPTIONS["feedback.ingest-topic.rollout-rate"] = 1.0 + ####################### # MaxMind Integration # ####################### From 0ce7b00b2aa5709b4a8582896fd8658ea0ae72be Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 13 Aug 2024 14:14:11 -0700 Subject: [PATCH 072/287] Migrate to zookeeper-less kafka (#3263) * migrate to kraft * fix syntax error * move zookeeper volume removal to end of installation --- _unit-test/create-docker-volumes-test.sh | 3 +- docker-compose.yml | 40 +++++++----------------- install/create-docker-volumes.sh | 1 - install/wrap-up.sh | 5 +++ 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/_unit-test/create-docker-volumes-test.sh b/_unit-test/create-docker-volumes-test.sh index 6b1176c5c73..2cb9b962a8b 100755 --- a/_unit-test/create-docker-volumes-test.sh +++ b/_unit-test/create-docker-volumes-test.sh @@ -14,8 +14,7 @@ sentry-data sentry-kafka sentry-postgres sentry-redis -sentry-symbolicator -sentry-zookeeper" +sentry-symbolicator" before=$(get_volumes) diff --git a/docker-compose.yml b/docker-compose.yml index 654220d4ded..be219e31138 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -132,42 +132,27 @@ services: POSTGRES_HOST_AUTH_METHOD: "trust" volumes: - "sentry-postgres:/var/lib/postgresql/data" - zookeeper: - <<: *restart_policy - image: "confluentinc/cp-zookeeper:7.6.1" - environment: - ZOOKEEPER_CLIENT_PORT: "2181" - CONFLUENT_SUPPORT_METRICS_ENABLE: "false" - ZOOKEEPER_LOG4J_ROOT_LOGLEVEL: "WARN" - ZOOKEEPER_TOOLS_LOG4J_LOGLEVEL: "WARN" - KAFKA_OPTS: "-Dzookeeper.4lw.commands.whitelist=ruok" - ulimits: - nofile: - soft: 4096 - hard: 4096 - volumes: - - "sentry-zookeeper:/var/lib/zookeeper/data" - - "sentry-zookeeper-log:/var/lib/zookeeper/log" - - "sentry-secrets:/etc/zookeeper/secrets" - healthcheck: - <<: *healthcheck_defaults - test: ["CMD-SHELL", 'echo "ruok" | nc -w 2 localhost 2181 | grep imok'] kafka: <<: *restart_policy - depends_on: - zookeeper: - <<: *depends_on-healthy image: "confluentinc/cp-kafka:7.6.1" environment: - KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" - KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://kafka:9092" + # https://docs.confluent.io/platform/current/installation/docker/config-reference.html#cp-kakfa-example + KAFKA_PROCESS_ROLES: "broker,controller" + KAFKA_CONTROLLER_QUORUM_VOTERS: "1001@127.0.0.1:29093" + KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" + KAFKA_NODE_ID: "1001" + CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk" + KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:29092,INTERNAL://0.0.0.0:9093,EXTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:29093" + KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://127.0.0.1:29092,INTERNAL://kafka:9093,EXTERNAL://kafka:9092" + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT" + KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1" KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS: "1" KAFKA_LOG_RETENTION_HOURS: "24" KAFKA_MESSAGE_MAX_BYTES: "50000000" #50MB or bust KAFKA_MAX_REQUEST_SIZE: "50000000" #50MB on requests apparently too CONFLUENT_SUPPORT_METRICS_ENABLE: "false" - KAFKA_LOG4J_LOGGERS: "kafka.cluster=WARN,kafka.controller=WARN,kafka.coordinator=WARN,kafka.log=WARN,kafka.server=WARN,kafka.zookeeper=WARN,state.change.logger=WARN" + KAFKA_LOG4J_LOGGERS: "kafka.cluster=WARN,kafka.controller=WARN,kafka.coordinator=WARN,kafka.log=WARN,kafka.server=WARN,state.change.logger=WARN" KAFKA_LOG4J_ROOT_LOGLEVEL: "WARN" KAFKA_TOOLS_LOG4J_LOGLEVEL: "WARN" ulimits: @@ -538,8 +523,6 @@ volumes: external: true sentry-redis: external: true - sentry-zookeeper: - external: true sentry-kafka: external: true sentry-clickhouse: @@ -555,7 +538,6 @@ volumes: sentry-secrets: sentry-smtp: sentry-nginx-cache: - sentry-zookeeper-log: sentry-kafka-log: sentry-smtp-log: sentry-clickhouse-log: diff --git a/install/create-docker-volumes.sh b/install/create-docker-volumes.sh index ca3ef0b23ed..15f20d54409 100644 --- a/install/create-docker-volumes.sh +++ b/install/create-docker-volumes.sh @@ -6,6 +6,5 @@ echo "Created $(docker volume create --name=sentry-kafka)." echo "Created $(docker volume create --name=sentry-postgres)." echo "Created $(docker volume create --name=sentry-redis)." echo "Created $(docker volume create --name=sentry-symbolicator)." -echo "Created $(docker volume create --name=sentry-zookeeper)." echo "${_endgroup}" diff --git a/install/wrap-up.sh b/install/wrap-up.sh index a811f81a618..8840262f25d 100644 --- a/install/wrap-up.sh +++ b/install/wrap-up.sh @@ -28,3 +28,8 @@ else echo "-----------------------------------------------------------------" echo "" fi + +# TODO(getsentry/self-hosted#2489) +if docker volume ls | grep -qw sentry-zookeeper; then + docker volume rm sentry-zookeeper +fi From b7ed3cb8b05c5f0e3f1d77b9ba970ae672bae0b7 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 15 Aug 2024 18:06:05 +0000 Subject: [PATCH 073/287] release: 24.8.0 --- .env | 10 +++++----- CHANGELOG.md | 13 +++++++++++++ README.md | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.env b/.env index db8bdb8dba2..79a394e0426 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.8.0 +SNUBA_IMAGE=getsentry/snuba:24.8.0 +RELAY_IMAGE=getsentry/relay:24.8.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.8.0 +VROOM_IMAGE=getsentry/vroom:24.8.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8925d795cd2..f13da018e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 24.8.0 + +### Various fixes & improvements + +- Migrate to zookeeper-less kafka (#3263) by @hubertdeng123 +- Revert "ref(feedback): cleanup topic rollout option" (#3262) by @aliu39 +- ref(feedback): cleanup topic rollout option (#3256) by @aliu39 +- Remove cdc and wal2json and use the default postgres entrypoint (#3260) by @beezz +- add `-euo pipefail` to enhance-image.example.sh (#3246) by @asottile-sentry +- remove python-dev (#3242) by @asottile-sentry +- feat: enable user feedback feature (#3193) by @aldy505 +- Use CDN by default for JS SDK Loader (#3213) by @stayallive + ## 24.7.1 ### Various fixes & improvements diff --git a/README.md b/README.md index 7315466c250..5fb7b56e10a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry 24.8.0 Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 2d8d3495bdacb1e1e05530b4b5023c275de9f47e Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 15 Aug 2024 11:49:41 -0700 Subject: [PATCH 074/287] Update release template (#3270) update release template --- .github/ISSUE_TEMPLATE/release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index c8285b5409e..48691f3255d 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -10,10 +10,8 @@ body: [previous YY.M.N](https://github.com/getsentry/self-hosted/issues) | ***YY.M.N*** | [next YY.M.N](https://github.com/getsentry/self-hosted/issues) - [ ] Release all components (_replace items with [publish repo issue links](https://github.com/getsentry/publish/issues)_). - - [ ] [`develop`](https://github.com/getsentry/develop/actions/workflows/prepare-release.yml) - [ ] [`relay`](https://github.com/getsentry/relay/actions/workflows/release_binary.yml) - [ ] [`sentry`](https://github.com/getsentry/sentry/actions/workflows/release.yml) - - [ ] [`sentry-docs`](https://github.com/getsentry/sentry-docs/actions/workflows/prepare-release.yml) - [ ] [`snuba`](https://github.com/getsentry/snuba/actions/workflows/release.yml) - [ ] [`symbolicator`](https://github.com/getsentry/symbolicator/actions/workflows/release.yml) - [ ] [`vroom`](https://github.com/getsentry/vroom/actions/workflows/release.yaml) @@ -25,7 +23,6 @@ body: - [ ] Follow up. - [ ] [Create the next release issue](https://github.com/getsentry/self-hosted/issues/new?assignees=&labels=&projects=&template=release.yml) and link it from this one. - _replace with link_ - - [ ] Update the [quarterly ticket](https://github.com/getsentry/team-ospo/issues). - [ ] Update the [release issue template](https://github.com/getsentry/self-hosted/blob/master/.github/ISSUE_TEMPLATE/release.yml). - [ ] Create a PR to update relocation release tests to add the new version. - _replace with link_ From 9b56d6293f7628a19a3b762a6c4faa1be50e04a4 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 15 Aug 2024 20:10:34 +0000 Subject: [PATCH 075/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 79a394e0426..db8bdb8dba2 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.8.0 -SNUBA_IMAGE=getsentry/snuba:24.8.0 -RELAY_IMAGE=getsentry/relay:24.8.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.8.0 -VROOM_IMAGE=getsentry/vroom:24.8.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/README.md b/README.md index 5fb7b56e10a..7315466c250 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Self-Hosted Sentry 24.8.0 +# Self-Hosted Sentry nightly Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). From 8595228be3b56aa33cf6d0327de0993bff29c747 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:58:28 -0700 Subject: [PATCH 076/287] ref(feedback): cleanup topic rollout option (#3276) --- sentry/sentry.conf.example.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index a200124f833..f20c4efdcb6 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -325,10 +325,6 @@ def get_internal_network(): } ) -# Temporary flag to mark User Feedback to be produced to the dedicated feedback topic by relay. -# This will be removed at a later time after it's considered stable and fully rolled out. -SENTRY_OPTIONS["feedback.ingest-topic.rollout-rate"] = 1.0 - ####################### # MaxMind Integration # ####################### From b6de547e45aef7897e635620f54cc09769de6bba Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Sat, 17 Aug 2024 12:26:50 -0700 Subject: [PATCH 077/287] Mandate minimum requirements for ram/cpu (#3275) --- install/_min-requirements.sh | 6 ++---- install/check-minimum-requirements.sh | 4 ---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/install/_min-requirements.sh b/install/_min-requirements.sh index f8836db0863..ae0b7c01a85 100644 --- a/install/_min-requirements.sh +++ b/install/_min-requirements.sh @@ -1,7 +1,5 @@ # Don't forget to update the README and othes docs when you change these! MIN_DOCKER_VERSION='19.03.6' MIN_COMPOSE_VERSION='2.19.0' -MIN_RAM_HARD=3800 # MB -MIN_RAM_SOFT=7800 # MB -MIN_CPU_HARD=2 -MIN_CPU_SOFT=4 +MIN_RAM_HARD=15900 # MB +MIN_CPU_HARD=4 diff --git a/install/check-minimum-requirements.sh b/install/check-minimum-requirements.sh index 01488e8d176..47848cc2859 100644 --- a/install/check-minimum-requirements.sh +++ b/install/check-minimum-requirements.sh @@ -36,16 +36,12 @@ CPU_AVAILABLE_IN_DOCKER=$(docker run --rm busybox nproc --all) if [[ "$CPU_AVAILABLE_IN_DOCKER" -lt "$MIN_CPU_HARD" ]]; then echo "FAIL: Required minimum CPU cores available to Docker is $MIN_CPU_HARD, found $CPU_AVAILABLE_IN_DOCKER" exit 1 -elif [[ "$CPU_AVAILABLE_IN_DOCKER" -lt "$MIN_CPU_SOFT" ]]; then - echo "WARN: Recommended minimum CPU cores available to Docker is $MIN_CPU_SOFT, found $CPU_AVAILABLE_IN_DOCKER" fi RAM_AVAILABLE_IN_DOCKER=$(docker run --rm busybox free -m 2>/dev/null | awk '/Mem/ {print $2}') if [[ "$RAM_AVAILABLE_IN_DOCKER" -lt "$MIN_RAM_HARD" ]]; then echo "FAIL: Required minimum RAM available to Docker is $MIN_RAM_HARD MB, found $RAM_AVAILABLE_IN_DOCKER MB" exit 1 -elif [[ "$RAM_AVAILABLE_IN_DOCKER" -lt "$MIN_RAM_SOFT" ]]; then - echo "WARN: Recommended minimum RAM available to Docker is $MIN_RAM_SOFT MB, found $RAM_AVAILABLE_IN_DOCKER MB" fi #SSE4.2 required by Clickhouse (https://clickhouse.yandex/docs/en/operations/requirements/) From 3cf323843ab9e63515bd2816a426bd5116324834 Mon Sep 17 00:00:00 2001 From: joshuarli Date: Fri, 23 Aug 2024 12:32:52 -0700 Subject: [PATCH 078/287] fix: more leeway for minimum RAM (#3290) more generous leeway --- install/_min-requirements.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/install/_min-requirements.sh b/install/_min-requirements.sh index ae0b7c01a85..2180c6a9a97 100644 --- a/install/_min-requirements.sh +++ b/install/_min-requirements.sh @@ -1,5 +1,9 @@ -# Don't forget to update the README and othes docs when you change these! +# Don't forget to update the README and other docs when you change these! MIN_DOCKER_VERSION='19.03.6' MIN_COMPOSE_VERSION='2.19.0' -MIN_RAM_HARD=15900 # MB + +# 16 GB minimum host RAM, but there'll be some overhead outside of what +# can be allotted to docker +MIN_RAM_HARD=14000 # MB + MIN_CPU_HARD=4 From b8b4aa20aa7aa4e37a8a2569d70ca54d450ae947 Mon Sep 17 00:00:00 2001 From: joshuarli Date: Tue, 3 Sep 2024 12:55:40 -0700 Subject: [PATCH 079/287] docs: link to develop docs (#3307) --- README.md | 82 ++----------------------------------------------------- 1 file changed, 3 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 7315466c250..a8f1fef5b1a 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,5 @@ -# Self-Hosted Sentry nightly +# Self-Hosted Sentry -Official bootstrap for running your own [Sentry](https://sentry.io/) with [Docker](https://www.docker.com/). +[Sentry](https://sentry.io/), feature-complete and packaged up for low-volume deployments and proofs-of-concept. -## Requirements - -* Docker 19.03.6+ -* Compose 2.19.0+ -* 4 CPU Cores -* 16 GB RAM -* 20 GB Free Disk Space - -## Setup - -### Installation - -To get started with all the defaults, simply clone the repo and run `./install.sh` in your local check-out. Please also read the section below about monitoring. Sentry uses Python 3 by default since December 4th, 2020 and Sentry 21.1.0 is the last version to support Python 2. - -During the install, a prompt will ask if you want to create a user account. If you require that the install goes on without creating a user, run `./install.sh --skip-user-creation`. - -Thinking of not managing this yourself? Check out the [SaaS migration docs](https://docs.sentry.io/product/sentry-basics/migration/) or [contact us](https://sentry.io/from/self-hosted) for help. - -Please visit [our documentation](https://develop.sentry.dev/self-hosted/) for everything else. - -### Customize DotEnv (.env) file - -Environment specific configurations can be done in the `.env.custom` file. It will be located in the root directory of the Sentry installation, and if it exists then `.env` will be ignored entirely. - -By default, there exists no `.env.custom` file. In this case, you can manually add this file by copying the `.env` file to a new `.env.custom` file and adjust your settings in the `.env.custom` file. - -Please keep in mind to check the `.env` file for changes, when you perform an upgrade of Sentry, so that you can adjust your `.env.custom` accordingly, if required, as `.env` is ignored entirely if `.env.custom` is present. - -### Enhance Sentry image - -To install plugins and their dependencies or make other modifications to the Sentry base image, -copy `sentry/enhance-image.example.sh` to `sentry/enhance-image.sh` and add necessary steps there. -For example, you can use `apt-get` to install dependencies and use `pip` to install plugins. - -After making modifications to `sentry/enhance-image.sh`, run `./install.sh` again to apply them. - -## Tips & Tricks - -### Event Retention - -Sentry comes with a cleanup cron job that prunes events older than `90 days` by default. If you want to change that, you can change the `SENTRY_EVENT_RETENTION_DAYS` environment variable in `.env` or simply override it in your environment. If you do not want the cleanup cron, you can remove the `sentry-cleanup` service from the `docker-compose.yml`file. - -### Installing a specific SHA - -If you want to install a specific release of Sentry, use the tags/releases on this repo. - -We continuously push the Docker image for each commit made into [Sentry](https://github.com/getsentry/sentry), and other services such as [Snuba](https://github.com/getsentry/snuba) or [Symbolicator](https://github.com/getsentry/symbolicator) to [our Docker Hub](https://hub.docker.com/u/getsentry) and tag the latest version on master as `:nightly`. This is also usually what we have on sentry.io and what the install script uses. You can use a custom Sentry image, such as a modified version that you have built on your own, or simply a specific commit hash by setting the `SENTRY_IMAGE` environment variable to that image name before running `./install.sh`: - -```shell -SENTRY_IMAGE=getsentry/sentry:83b1380 ./install.sh -``` - -Note that this may not work for all commit SHAs as this repository evolves with Sentry and its satellite projects. It is highly recommended to check out a version of this repository that is close to the timestamp of the Sentry commit you are installing. - -### Using Linux - -If you are using Linux and you need to use `sudo` when running `./install.sh`, make sure to place the environment variable *after* `sudo`: - -```shell -sudo SENTRY_IMAGE=us.gcr.io/sentryio/sentry:83b1380 ./install.sh -``` - -Where you replace `83b1380` with the sha you want to use. - -### Self-Hosted Monitoring - -We'd love to catch errors in self-hosted so you don't run into them, and so we can fix them faster! When you run `./install.sh`, you will be prompted to select whether to opt in or out of our monitoring. If you opt into our monitoring, we will send information to our own self-hosted Sentry instance for development and debugging purposes. We may collect: - -- OS username -- IP address -- install log -- runtime errors in Sentry -- performance data - -Thirty (30) day retention. No marketing. Privacy policy at sentry.io/privacy. - -Starting with the 22.10.0 release in October, we will require those running the Sentry installer to choose to opt in or out. If you are running the installer under automation, you may want to set `REPORT_SELF_HOSTED_ISSUES` or pass `--(no-)report-self-hosted-issues` to the installer accordingly. +Documentation [here](https://develop.sentry.dev/self-hosted/). From 8887a82a86153d0c9696463c0525976cf8e3f136 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sun, 15 Sep 2024 18:05:44 +0000 Subject: [PATCH 080/287] release: 24.9.0 --- .env | 10 +++++----- CHANGELOG.md | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.env b/.env index db8bdb8dba2..5adf46197a9 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.9.0 +SNUBA_IMAGE=getsentry/snuba:24.9.0 +RELAY_IMAGE=getsentry/relay:24.9.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.9.0 +VROOM_IMAGE=getsentry/vroom:24.9.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index f13da018e5e..5ff69d1f2ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 24.9.0 + +### Various fixes & improvements + +- docs: link to develop docs (#3307) by @joshuarli +- fix: more leeway for minimum RAM (#3290) by @joshuarli +- Mandate minimum requirements for ram/cpu (#3275) by @hubertdeng123 +- ref(feedback): cleanup topic rollout option (#3276) by @aliu39 +- Update release template (#3270) by @hubertdeng123 + ## 24.8.0 ### Various fixes & improvements From 5bd6cd3710cc214b2d68858d24a7d6bcf8149d73 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 16 Sep 2024 22:55:23 +0000 Subject: [PATCH 081/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5adf46197a9..db8bdb8dba2 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.9.0 -SNUBA_IMAGE=getsentry/snuba:24.9.0 -RELAY_IMAGE=getsentry/relay:24.9.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.9.0 -VROOM_IMAGE=getsentry/vroom:24.9.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 036f6d49e9b4388d81a0bc907ea0e274ba55c401 Mon Sep 17 00:00:00 2001 From: Nikhar Saxena <84807402+nikhars@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:00:32 -0700 Subject: [PATCH 082/287] fix(clickhouse): Allow nullable key (#3354) --- clickhouse/config.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clickhouse/config.xml b/clickhouse/config.xml index 19a0ecf80a9..28ec384cb62 100644 --- a/clickhouse/config.xml +++ b/clickhouse/config.xml @@ -16,6 +16,8 @@ + 1 + 0 From 5910c02cc404e533348de84d99acb6d9f7b85530 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 8 Oct 2024 06:03:28 +0700 Subject: [PATCH 083/287] ref: allow hosted js sdk bundles (#3365) * ref: allow hosted js sdk bundles --------- Co-authored-by: Burak Yigit Kaya --- .env | 2 ++ _unit-test/js-sdk-assets-test.sh | 26 ++++++++++++++++++++++ docker-compose.yml | 4 ++++ install.sh | 1 + install/setup-js-sdk-assets.sh | 38 ++++++++++++++++++++++++++++++++ nginx/nginx.conf | 4 ++++ sentry/sentry.conf.example.py | 11 +++++++++ 7 files changed, 86 insertions(+) create mode 100755 _unit-test/js-sdk-assets-test.sh create mode 100644 install/setup-js-sdk-assets.sh diff --git a/.env b/.env index db8bdb8dba2..58e11832a61 100644 --- a/.env +++ b/.env @@ -17,3 +17,5 @@ HEALTHCHECK_RETRIES=10 # Caution: Raising max connections of postgres increases CPU and RAM usage # see https://github.com/getsentry/self-hosted/pull/2740 for more information POSTGRES_MAX_CONNECTIONS=100 +# Set SETUP_JS_SDK_ASSETS to 1 to enable the setup of JS SDK assets +# SETUP_JS_SDK_ASSETS=1 diff --git a/_unit-test/js-sdk-assets-test.sh b/_unit-test/js-sdk-assets-test.sh new file mode 100755 index 00000000000..5355a57076d --- /dev/null +++ b/_unit-test/js-sdk-assets-test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +source _unit-test/_test_setup.sh +source install/dc-detect-version.sh +$dcb --force-rm web + +export SETUP_JS_SDK_ASSETS=1 + +source install/setup-js-sdk-assets.sh + +sdk_files=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx ls -lah /var/www/js-sdk/) +sdk_tree=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx tree /var/www/js-sdk/ | tail -n 1) + +# `sdk_files` should contains 2 lines, `7.*` and `8.*` +echo $sdk_files +total_directories=$(echo "$sdk_files" | grep -c '[78]\.[0-9]*\.[0-9]*$') +echo $total_directories +test "2" == "$total_directories" +echo "Pass" + +# `sdk_tree` should outputs "2 directories, 10 files" +echo "$sdk_tree" +test "2 directories, 10 files" == "$(echo "$sdk_tree")" +echo "Pass" + +report_success diff --git a/docker-compose.yml b/docker-compose.yml index be219e31138..e38d4af0f36 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -460,6 +460,7 @@ services: source: ./nginx target: /etc/nginx - sentry-nginx-cache:/var/cache/nginx + - sentry-nginx-www:/var/www depends_on: - web - relay @@ -529,6 +530,9 @@ volumes: external: true sentry-symbolicator: external: true + # This volume stores JS SDK assets and the data inside this volume should + # be cleaned periodically on upgrades. + sentry-nginx-www: # This volume stores profiles and should be persisted. # Not being external will still persist data across restarts. # It won't persist if someone does a docker compose down -v. diff --git a/install.sh b/install.sh index ee2e008d1e7..e9460d36ceb 100755 --- a/install.sh +++ b/install.sh @@ -36,4 +36,5 @@ source install/bootstrap-snuba.sh source install/upgrade-postgres.sh source install/set-up-and-migrate-database.sh source install/geoip.sh +source install/setup-js-sdk-assets.sh source install/wrap-up.sh diff --git a/install/setup-js-sdk-assets.sh b/install/setup-js-sdk-assets.sh new file mode 100644 index 00000000000..c8521ce8a52 --- /dev/null +++ b/install/setup-js-sdk-assets.sh @@ -0,0 +1,38 @@ +# This will only run if the SETUP_JS_SDK_ASSETS environment variable is set to 1. +# Think of this as some kind of a feature flag. +if [[ "${SETUP_JS_SDK_ASSETS:-}" == "1" ]]; then + echo "${_group}Setting up JS SDK assets" + + # If the `sentry-nginx-www` volume exists, we need to prune the contents. + # We don't want to fill the volume with old JS SDK assets. + # If people want to keep the old assets, they can set the environment variable + # `SETUP_JS_SDK_KEEP_OLD_ASSETS` to any value. + if [[ -z "${SETUP_JS_SDK_KEEP_OLD_ASSETS:-}" ]]; then + echo "Cleaning up old JS SDK assets..." + $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx rm -rf /var/www/js-sdk/* + fi + + $dbuild -t sentry-self-hosted-jq-local --platform="$DOCKER_PLATFORM" jq + + jq="docker run --rm -i sentry-self-hosted-jq-local" + + loader_registry=$($dcr --no-deps --rm -T web cat /usr/src/sentry/src/sentry/loader/_registry.json) + # The `loader_registry` should start with "Updating certificates...", we want to delete that and the subsequent ca-certificates related lines. + # We want to remove everything before the first '{'. + loader_registry=$(echo "$loader_registry" | sed '0,/{/s/[^{]*//') + + latest_js_v7=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("7.")))) | .[0]') + latest_js_v8=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("8.")))) | .[0]') + + echo "Found JS SDKs v${latest_js_v7} and v${latest_js_v8}, downloading from upstream.." + + # Download those two using wget + for version in "${latest_js_v7}" "${latest_js_v8}"; do + $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx mkdir -p /var/www/js-sdk/${version} + for variant in "tracing" "tracing.replay" "replay" "tracing.replay.feedback" "feedback"; do + $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx wget -q -O /var/www/js-sdk/${version}/bundle.${variant}.min.js "https://browser.sentry-cdn.com/$%7Bversion%7D/bundle.$%7Bvariant%7D.min.js" + done + done + + echo "${_endgroup}" +fi diff --git a/nginx/nginx.conf b/nginx/nginx.conf index c24fe7ec188..66bb4301e82 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -88,6 +88,10 @@ http { location ^~ /api/0/relays/ { proxy_pass http://relay; } + location ^~ /js-sdk/ { + autoindex on; + root /var/www/js-sdk; + } location / { proxy_pass http://sentry; } diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index f20c4efdcb6..bb3b4fd5d9b 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -378,6 +378,17 @@ def get_internal_network(): # JS SDK Loader # ################# +# Configure Sentry JS SDK bundle URL template for Loader Scripts. +# Learn more about the Loader Scripts: https://docs.sentry.io/platforms/javascript/install/loader/ + +# If you wish to host your own JS SDK bundles, set `SETUP_JS_SDK_ASSETS` environment variable to `1` +# on your `.env` or `.env.custom` file. Then, replace the value below with your own public URL. +# For example: "https://sentry.example.com/js-sdk/%s/bundle%s.min.js" +# +# By default, the previous JS SDK assets version will be pruned during upgrades, if you wish +# to keep the old assets, set `SETUP_JS_SDK_KEEP_OLD_ASSETS` environment variable to any value on +# your `.env` or `.env.custom` file. The files should only be a few KBs, and this might be useful +# if you're using it directly like a CDN instead of using the loader script. JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" From cba2d4b236548cfe8964bc8cdaa576a4fcdc112a Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 9 Oct 2024 01:54:07 +0700 Subject: [PATCH 084/287] docs: explicitly specify `mail.use-{tls,ssl}` is mutually exclusive (#3368) --- sentry/config.example.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry/config.example.yml b/sentry/config.example.yml index de0b68b99f8..d5a6dc322ce 100644 --- a/sentry/config.example.yml +++ b/sentry/config.example.yml @@ -12,6 +12,8 @@ mail.host: 'smtp' # mail.port: 25 # mail.username: '' # mail.password: '' +# NOTE: `mail.use-tls` and `mail.use-ssl` are mutually exclusive and should not +# appear at the same time. Only uncomment one of them. # mail.use-tls: false # mail.use-ssl: false @@ -19,7 +21,6 @@ mail.host: 'smtp' # through SENTRY_MAIL_HOST in sentry.conf.py so remove those first if # you want your values in this file to be effective! - # The email address to send on behalf of # mail.from: 'root@localhost' or ... # mail.from: 'System Administrator ' From 9a2f4e184452aa11fde5b2edbea5ea101f0ee5fe Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 10 Oct 2024 21:54:39 +0700 Subject: [PATCH 085/287] ref: span normalization allowed host config (#3245) Co-authored-by: Hubert Deng --- sentry/sentry.conf.example.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index bb3b4fd5d9b..222acf0a878 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -380,7 +380,6 @@ def get_internal_network(): # Configure Sentry JS SDK bundle URL template for Loader Scripts. # Learn more about the Loader Scripts: https://docs.sentry.io/platforms/javascript/install/loader/ - # If you wish to host your own JS SDK bundles, set `SETUP_JS_SDK_ASSETS` environment variable to `1` # on your `.env` or `.env.custom` file. Then, replace the value below with your own public URL. # For example: "https://sentry.example.com/js-sdk/%s/bundle%s.min.js" @@ -394,3 +393,15 @@ def get_internal_network(): # If you would like to use self-hosted Sentry with only errors enabled, please set this SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete" + +##################### +# Insights Settings # +##################### + +# Since version 24.3.0, Insights features are available on self-hosted. For Requests module, +# there are scrubbing logic done on Relay to prevent high cardinality of stored HTTP hosts. +# However in self-hosted scenario, the amount of stored HTTP hosts might be consistent, +# and you may have allow list of hosts that you want to keep. Uncomment the following line +# to allow specific hosts. It might be IP addresses or domain names (without `http://` or `https://`). + +# SENTRY_OPTIONS["relay.span-normalization.allowed_hosts"] = ["example.com", "192.168.10.1"] From bdf8d3ff918d92b04d10906f3869900ca41ec3bf Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 10 Oct 2024 16:55:58 +0200 Subject: [PATCH 086/287] chore: replace old URLs of the repo with the new docs (#3375) --- sentry/Dockerfile | 2 +- sentry/enhance-image.example.sh | 2 +- sentry/entrypoint.sh | 2 +- sentry/requirements.example.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/Dockerfile b/sentry/Dockerfile index 5afe9c2dd86..557046f143d 100644 --- a/sentry/Dockerfile +++ b/sentry/Dockerfile @@ -8,6 +8,6 @@ RUN if [ -s /usr/src/sentry/enhance-image.sh ]; then \ fi RUN if [ -s /usr/src/sentry/requirements.txt ]; then \ - echo "sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://github.com/getsentry/self-hosted#enhance-sentry-image"; \ + echo "sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://develop.sentry.dev/self-hosted/#enhance-sentry-image"; \ pip install -r /usr/src/sentry/requirements.txt; \ fi diff --git a/sentry/enhance-image.example.sh b/sentry/enhance-image.example.sh index c3ae96c96da..de17136d9c9 100755 --- a/sentry/enhance-image.example.sh +++ b/sentry/enhance-image.example.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail -# Enhance the base $SENTRY_IMAGE with additional dependencies, plugins - see https://github.com/getsentry/self-hosted#enhance-sentry-image +# Enhance the base $SENTRY_IMAGE with additional dependencies, plugins - see https://develop.sentry.dev/self-hosted/#enhance-sentry-image # For example: # apt-get update # apt-get install -y gcc libsasl2-dev libldap2-dev libssl-dev diff --git a/sentry/entrypoint.sh b/sentry/entrypoint.sh index 552de05b69a..7be738fdd7e 100755 --- a/sentry/entrypoint.sh +++ b/sentry/entrypoint.sh @@ -6,7 +6,7 @@ if [ "$(ls -A /usr/local/share/ca-certificates/)" ]; then fi if [ -e /etc/sentry/requirements.txt ]; then - echo "sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://github.com/getsentry/self-hosted#enhance-sentry-image" + echo "sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://develop.sentry.dev/self-hosted/#enhance-sentry-image" fi source /docker-entrypoint.sh diff --git a/sentry/requirements.example.txt b/sentry/requirements.example.txt index e7b63dc9a67..393a2f52fc3 100644 --- a/sentry/requirements.example.txt +++ b/sentry/requirements.example.txt @@ -1 +1 @@ -# sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://github.com/getsentry/self-hosted#enhance-sentry-image +# sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://develop.sentry.dev/self-hosted/#enhance-sentry-image From 73f9f0067264bcbb541bf1c5ca57520abd542e68 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 17 Oct 2024 14:17:20 -0700 Subject: [PATCH 087/287] chore: Disable codecov for master/release branches (#3384) * disable codecov for master/release branches * add new line * use default project * also do changes for patch --- codecov.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..ed9aed0e588 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + only_pulls: true + patch: + default: + only_pulls: true From a29a7969f5f7a93e27c67f0aa622452b5ddc2687 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 17 Oct 2024 21:17:47 +0000 Subject: [PATCH 088/287] release: 24.10.0 --- .env | 10 +++++----- CHANGELOG.md | 11 +++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 58e11832a61..01d06061204 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.10.0 +SNUBA_IMAGE=getsentry/snuba:24.10.0 +RELAY_IMAGE=getsentry/relay:24.10.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.10.0 +VROOM_IMAGE=getsentry/vroom:24.10.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ff69d1f2ea..8c738fe7ab9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 24.10.0 + +### Various fixes & improvements + +- chore: Disable codecov for master/release branches (#3384) by @hubertdeng123 +- chore: replace old URLs of the repo with the new docs (#3375) by @victorelec14 +- ref: span normalization allowed host config (#3245) by @aldy505 +- docs: explicitly specify `mail.use-{tls,ssl}` is mutually exclusive (#3368) by @aldy505 +- ref: allow hosted js sdk bundles (#3365) by @aldy505 +- fix(clickhouse): Allow nullable key (#3354) by @nikhars + ## 24.9.0 ### Various fixes & improvements From f0f854c6f44bff0c47366fd788e95243473f2072 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 17 Oct 2024 21:43:41 +0000 Subject: [PATCH 089/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 01d06061204..58e11832a61 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.10.0 -SNUBA_IMAGE=getsentry/snuba:24.10.0 -RELAY_IMAGE=getsentry/relay:24.10.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.10.0 -VROOM_IMAGE=getsentry/vroom:24.10.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 8fd24d02312f9fd7990c1ad0808d561c7b4f80b5 Mon Sep 17 00:00:00 2001 From: Daniil Makhonia <32175481+Makhonya@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:09:31 +0200 Subject: [PATCH 090/287] fix(sentry-admin): Do not wait for command finish to display output (#3390) Currently sentry-admin.sh script saves the output of the command to a separate variable. This makes the command "freeze" if it requires any input from the user (like sentry-admin.sh restore). The change should provide the output directly to the shell. Also changed contributing.md as it seemed outdated and updated requirements-dev.txt with the missing `cryptography` package. Co-authored-by: Daniil Makhonia --- CONTRIBUTING.md | 12 +++++++++--- requirements-dev.txt | 1 + sentry-admin.sh | 3 +-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 49120afa854..3c3e987b2a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,13 @@ ## Testing -Validate changes to the setup by running the integration test: +### Running Tests with Pytest -```shell -./integration-test.sh +We use pytest for running tests. To run the tests: + +1) Ensure that you are in the root directory of the project. +2) Run the following command: +```bash +pytest ``` + +This will automatically discover and run all test cases in the project. diff --git a/requirements-dev.txt b/requirements-dev.txt index 42bf67b2472..62b4166f202 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ pytest-rerunfailures>=11.0 pytest-sentry>=0.1.11 httpx>=0.25.2 beautifulsoup4>=4.7.1 +cryptography>=43.0.3 diff --git a/sentry-admin.sh b/sentry-admin.sh index d162c82114d..d775e905e94 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -22,8 +22,7 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { - output=$($dc run -v "$VOLUME_MAPPING" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1) - echo "$output" + $dc run -v "$VOLUME_MAPPING" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 } # Function to modify lines starting with `Usage: sentry` to say `Usage: ./sentry-admin.sh` instead. From 72c5e59ed41e966cf4e0857b69d9258ff16e2ea4 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:58:38 -0700 Subject: [PATCH 091/287] ref(feedback): remove issue platform flags after releasing issue types (#3397) --- sentry/sentry.conf.example.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 222acf0a878..28403d69451 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -318,9 +318,6 @@ def get_internal_network(): "organizations:user-feedback-ingest", "organizations:user-feedback-replay-clip", "organizations:user-feedback-ui", - "organizations:feedback-visible", - "organizations:feedback-ingest", - "organizations:feedback-post-process-group", ) } ) From 7574a49542319aab09c879a17ac40658e9f6d0d9 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 28 Oct 2024 20:02:23 +0000 Subject: [PATCH 092/287] Revert "ref(feedback): remove issue platform flags after releasing issue types" (#3402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "ref(feedback): remove issue platform flags after releasing issue type…" This reverts commit 72c5e59ed41e966cf4e0857b69d9258ff16e2ea4. https://github.com/getsentry/self-hosted/pull/3397#issuecomment-2442142424 --- sentry/sentry.conf.example.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 28403d69451..222acf0a878 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -318,6 +318,9 @@ def get_internal_network(): "organizations:user-feedback-ingest", "organizations:user-feedback-replay-clip", "organizations:user-feedback-ui", + "organizations:feedback-visible", + "organizations:feedback-ingest", + "organizations:feedback-post-process-group", ) } ) From db21c6cc5552d5279a9e5594c6860dd6f6382b52 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 31 Oct 2024 22:32:29 +0000 Subject: [PATCH 093/287] Revert "Revert "ref(feedback): remove issue platform flags after releasing issue types"" (#3403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "Revert "ref(feedback): remove issue platform flags after releasing is…" This reverts commit 7574a49542319aab09c879a17ac40658e9f6d0d9. --- sentry/sentry.conf.example.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 222acf0a878..28403d69451 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -318,9 +318,6 @@ def get_internal_network(): "organizations:user-feedback-ingest", "organizations:user-feedback-replay-clip", "organizations:user-feedback-ui", - "organizations:feedback-visible", - "organizations:feedback-ingest", - "organizations:feedback-post-process-group", ) } ) From 2a7abf215e6e1d2973ff457856e67488a72248be Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 7 Nov 2024 17:11:07 +0700 Subject: [PATCH 094/287] fix(loader): provide js sdk assets from 4.x (#3415) Hopefully fixes https://github.com/getsentry/sentry/issues/22715#issuecomment-2458066842 ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- _unit-test/js-sdk-assets-test.sh | 10 +++++----- install/setup-js-sdk-assets.sh | 26 +++++++++++++++++++++----- unit-test.sh | 5 +++-- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/_unit-test/js-sdk-assets-test.sh b/_unit-test/js-sdk-assets-test.sh index 5355a57076d..30c2f07e466 100755 --- a/_unit-test/js-sdk-assets-test.sh +++ b/_unit-test/js-sdk-assets-test.sh @@ -11,16 +11,16 @@ source install/setup-js-sdk-assets.sh sdk_files=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx ls -lah /var/www/js-sdk/) sdk_tree=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx tree /var/www/js-sdk/ | tail -n 1) -# `sdk_files` should contains 2 lines, `7.*` and `8.*` +# `sdk_files` should contains 5 lines, '4.*', '5.*', '6.*', `7.*` and `8.*` echo $sdk_files -total_directories=$(echo "$sdk_files" | grep -c '[78]\.[0-9]*\.[0-9]*$') +total_directories=$(echo "$sdk_files" | grep -c '[45678]\.[0-9]*\.[0-9]*$') echo $total_directories -test "2" == "$total_directories" +test "5" == "$total_directories" echo "Pass" -# `sdk_tree` should outputs "2 directories, 10 files" +# `sdk_tree` should output "5 directories, 17 files" echo "$sdk_tree" -test "2 directories, 10 files" == "$(echo "$sdk_tree")" +test "5 directories, 17 files" == "$(echo "$sdk_tree")" echo "Pass" report_success diff --git a/install/setup-js-sdk-assets.sh b/install/setup-js-sdk-assets.sh index c8521ce8a52..f9261393f5e 100644 --- a/install/setup-js-sdk-assets.sh +++ b/install/setup-js-sdk-assets.sh @@ -21,16 +21,32 @@ if [[ "${SETUP_JS_SDK_ASSETS:-}" == "1" ]]; then # We want to remove everything before the first '{'. loader_registry=$(echo "$loader_registry" | sed '0,/{/s/[^{]*//') + # Sentry backend provides SDK versions from v4.x up to v8.x. + latest_js_v4=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("4.")))) | .[0]') + latest_js_v5=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("5.")))) | .[0]') + latest_js_v6=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("6.")))) | .[0]') latest_js_v7=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("7.")))) | .[0]') latest_js_v8=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("8.")))) | .[0]') - echo "Found JS SDKs v${latest_js_v7} and v${latest_js_v8}, downloading from upstream.." + echo "Found JS SDKs: v${latest_js_v4}, v${latest_js_v5}, v${latest_js_v6}, v${latest_js_v7}, v${latest_js_v8}" - # Download those two using wget - for version in "${latest_js_v7}" "${latest_js_v8}"; do + versions=("$latest_js_v4" "$latest_js_v5" "$latest_js_v6" "$latest_js_v7" "$latest_js_v8") + variants=("bundle" "bundle.tracing" "bundle.tracing.replay" "bundle.replay" "bundle.tracing.replay.feedback" "bundle.feedback") + + # Download those versions & variants using curl + for version in "${versions[@]}"; do $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx mkdir -p /var/www/js-sdk/${version} - for variant in "tracing" "tracing.replay" "replay" "tracing.replay.feedback" "feedback"; do - $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx wget -q -O /var/www/js-sdk/${version}/bundle.${variant}.min.js "https://browser.sentry-cdn.com/$%7Bversion%7D/bundle.$%7Bvariant%7D.min.js" + for variant in "${variants[@]}"; do + # We want to have a HEAD lookup. If the response status code is not 200, we will skip the variant. + # Taken from https://superuser.com/questions/272265/getting-curl-to-output-http-status-code#comment1025992_272273 + status_code=$($dcr --no-deps --rm nginx curl --retry 5 -sLI "https://browser.sentry-cdn.com/$%7Bversion%7D/$%7Bvariant%7D.min.js" 2>/dev/null | head -n 1 | cut -d$' ' -f2) + if [[ "$status_code" != "200" ]]; then + echo "Skipping download of JS SDK v${version} for ${variant}.min.js, because the status code was ${status_code} (non 200)" + continue + fi + + echo "Downloading JS SDK v${version} for ${variant}.min.js..." + $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx curl --retry 10 -sLo /var/www/js-sdk/${version}/${variant}.min.js "https://browser.sentry-cdn.com/$%7Bversion%7D/$%7Bvariant%7D.min.js" done done diff --git a/unit-test.sh b/unit-test.sh index 6cb81fa8d20..01a945e3777 100755 --- a/unit-test.sh +++ b/unit-test.sh @@ -11,8 +11,9 @@ for test_file in _unit-test/*-test.sh; do fi echo "🙈 Running $test_file ..." $test_file - if [ $? != 0 ]; then - echo fail 👎 + exit_code=$? + if [ $exit_code != 0 ]; then + echo fail 👎 with exit code $exit_code fail=1 fi done From 49e30a7356aa0e2c58c7b0e1769dc0a98dcfd493 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 7 Nov 2024 12:25:53 +0000 Subject: [PATCH 095/287] fix: Use js.sentry-cdn.com for JS SDK downloads (#3417) Since we download JS SDKs in a for loop which invokes a separate docker container for each `curl` run, we seem to be triggering some sort of a DoS protection. And rightfully so as the old method causes TCP and TLS churn although we advertise we support HTTP/1.1 and HTTP/2. This patch does a few things: 1. Uses `curl`s globbing support to download all files in one go, maxing TCP and TLS reuse. This should fix the DoS protection 2. Uses `curl`'s `--compress` option to make things even more efficient 3. Uses `curl`'s `--create-dirs` to save 1 docker container run per version for creating the directory 4. Removes the `-I` `HEAD` checks in favor of a `-f` fail option combined with `|| true` which makes curl fail and not write the output on a non-200 response while still allowing the script to succeed 5. To make sure the above approach works, it adds a file size test, requiring all downloaded files to be larger than 1kB --- _unit-test/js-sdk-assets-test.sh | 6 ++++++ install/setup-js-sdk-assets.sh | 20 +++----------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/_unit-test/js-sdk-assets-test.sh b/_unit-test/js-sdk-assets-test.sh index 30c2f07e466..7177f559ba9 100755 --- a/_unit-test/js-sdk-assets-test.sh +++ b/_unit-test/js-sdk-assets-test.sh @@ -10,6 +10,7 @@ source install/setup-js-sdk-assets.sh sdk_files=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx ls -lah /var/www/js-sdk/) sdk_tree=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx tree /var/www/js-sdk/ | tail -n 1) +non_empty_file_count=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk/ -type f -size +1k | wc -l) # `sdk_files` should contains 5 lines, '4.*', '5.*', '6.*', `7.*` and `8.*` echo $sdk_files @@ -23,4 +24,9 @@ echo "$sdk_tree" test "5 directories, 17 files" == "$(echo "$sdk_tree")" echo "Pass" +# Files should all be >1k (ensure they are not empty) +echo "Testing file sizes" +test "17" == "$non_empty_file_count" +echo "Pass" + report_success diff --git a/install/setup-js-sdk-assets.sh b/install/setup-js-sdk-assets.sh index f9261393f5e..50b9428dd4b 100644 --- a/install/setup-js-sdk-assets.sh +++ b/install/setup-js-sdk-assets.sh @@ -30,25 +30,11 @@ if [[ "${SETUP_JS_SDK_ASSETS:-}" == "1" ]]; then echo "Found JS SDKs: v${latest_js_v4}, v${latest_js_v5}, v${latest_js_v6}, v${latest_js_v7}, v${latest_js_v8}" - versions=("$latest_js_v4" "$latest_js_v5" "$latest_js_v6" "$latest_js_v7" "$latest_js_v8") - variants=("bundle" "bundle.tracing" "bundle.tracing.replay" "bundle.replay" "bundle.tracing.replay.feedback" "bundle.feedback") + versions="{$latest_js_v4,$latest_js_v5,$latest_js_v6,$latest_js_v7,$latest_js_v8}" + variants="{bundle,bundle.tracing,bundle.tracing.replay,bundle.replay,bundle.tracing.replay.feedback,bundle.feedback}" # Download those versions & variants using curl - for version in "${versions[@]}"; do - $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx mkdir -p /var/www/js-sdk/${version} - for variant in "${variants[@]}"; do - # We want to have a HEAD lookup. If the response status code is not 200, we will skip the variant. - # Taken from https://superuser.com/questions/272265/getting-curl-to-output-http-status-code#comment1025992_272273 - status_code=$($dcr --no-deps --rm nginx curl --retry 5 -sLI "https://browser.sentry-cdn.com/$%7Bversion%7D/$%7Bvariant%7D.min.js" 2>/dev/null | head -n 1 | cut -d$' ' -f2) - if [[ "$status_code" != "200" ]]; then - echo "Skipping download of JS SDK v${version} for ${variant}.min.js, because the status code was ${status_code} (non 200)" - continue - fi - - echo "Downloading JS SDK v${version} for ${variant}.min.js..." - $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx curl --retry 10 -sLo /var/www/js-sdk/${version}/${variant}.min.js "https://browser.sentry-cdn.com/$%7Bversion%7D/$%7Bvariant%7D.min.js" - done - done + $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx curl -w '%{response_code} %{url}\n' --no-progress-meter --compressed --retry 3 --create-dirs -fLo "/var/www/js-sdk/#1/#2.min.js" "https://browser.sentry-cdn.com/$%7Bversions%7D/$%7Bvariants%7D.min.js" || true echo "${_endgroup}" fi From 98f6cf0a5dcf209139b0f71bf2a1a50ec672f58f Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 7 Nov 2024 20:25:50 +0700 Subject: [PATCH 096/287] fix: missing mime types and turning off autoindex for js-sdk endpoint (#3395) Things weren't as smooth as I thought it would be. ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- docker-compose.yml | 4 ++-- nginx/nginx.conf => nginx.conf | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) rename nginx/nginx.conf => nginx.conf (88%) diff --git a/docker-compose.yml b/docker-compose.yml index e38d4af0f36..a899ae80410 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -457,8 +457,8 @@ services: volumes: - type: bind read_only: true - source: ./nginx - target: /etc/nginx + source: ./nginx.conf + target: /etc/nginx/nginx.conf - sentry-nginx-cache:/var/cache/nginx - sentry-nginx-www:/var/www depends_on: diff --git a/nginx/nginx.conf b/nginx.conf similarity index 88% rename from nginx/nginx.conf rename to nginx.conf index 66bb4301e82..32d9487f809 100644 --- a/nginx/nginx.conf +++ b/nginx.conf @@ -11,6 +11,7 @@ events { http { + include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' @@ -89,8 +90,10 @@ http { proxy_pass http://relay; } location ^~ /js-sdk/ { - autoindex on; - root /var/www/js-sdk; + root /var/www/; + # This value is set to mimic the behavior of the upstream Sentry CDN. For security reasons, + # it is recommended to change this to your Sentry URL (in most cases same as system.url-prefix). + add_header Access-Control-Allow-Origin *; } location / { proxy_pass http://sentry; From 6a8c3a47587bf23847e51a7de3a21d152f1b7064 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Mon, 11 Nov 2024 15:47:07 -0800 Subject: [PATCH 097/287] feat(healthcheck): Improve redis healthcheck (#3422) improve redis healthcheck --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index a899ae80410..9e4ae9dd730 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,7 +107,7 @@ services: image: "redis:6.2.14-alpine" healthcheck: <<: *healthcheck_defaults - test: redis-cli ping + test: redis-cli ping | grep PONG volumes: - "sentry-redis:/data" ulimits: From fa61f1707f8dd2c08622fa32695b3876bca80919 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 15 Nov 2024 18:06:09 +0000 Subject: [PATCH 098/287] release: 24.11.0 --- .env | 10 +++++----- CHANGELOG.md | 13 +++++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 58e11832a61..329686f40a1 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.11.0 +SNUBA_IMAGE=getsentry/snuba:24.11.0 +RELAY_IMAGE=getsentry/relay:24.11.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.11.0 +VROOM_IMAGE=getsentry/vroom:24.11.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c738fe7ab9..79a21509417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 24.11.0 + +### Various fixes & improvements + +- feat(healthcheck): Improve redis healthcheck (#3422) by @hubertdeng123 +- fix: missing mime types and turning off autoindex for js-sdk endpoint (#3395) by @aldy505 +- fix: Use js.sentry-cdn.com for JS SDK downloads (#3417) by @BYK +- fix(loader): provide js sdk assets from 4.x (#3415) by @aldy505 +- Revert "Revert "ref(feedback): remove issue platform flags after releasing issue types"" (#3403) by @BYK +- Revert "ref(feedback): remove issue platform flags after releasing issue types" (#3402) by @BYK +- ref(feedback): remove issue platform flags after releasing issue types (#3397) by @aliu39 +- fix(sentry-admin): Do not wait for command finish to display output (#3390) by @Makhonya + ## 24.10.0 ### Various fixes & improvements From 7cfab8db1ebfc115c32edce0bc242ccde7835d4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:41:33 +0000 Subject: [PATCH 099/287] build(deps): bump codecov/codecov-action from 4 to 5 (#3429) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e565392c897..291e78c7c8f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -147,7 +147,7 @@ jobs: docker compose logs - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} slug: getsentry/self-hosted From be66069eefe7b703e977af7f3d64d5002a549631 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 19 Nov 2024 02:09:53 +0000 Subject: [PATCH 100/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 329686f40a1..58e11832a61 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.11.0 -SNUBA_IMAGE=getsentry/snuba:24.11.0 -RELAY_IMAGE=getsentry/relay:24.11.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.11.0 -VROOM_IMAGE=getsentry/vroom:24.11.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From c3814f08077ce42428af4c583699f810e8dba78e Mon Sep 17 00:00:00 2001 From: Sajjad hassanzadeh <32982356+Hassanzadeh-sd@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:13:58 +0330 Subject: [PATCH 101/287] feat: add Redis configuration for improved memory management (#3427) As Sentry continues to evolve, effective resource management becomes crucial for maintaining performance and stability. This update includes configurations that will help optimize Redis's memory usage, ensuring that the system runs efficiently under varying loads. **Key Changes:** - **Added `maxmemory` Directive**: Configured Redis to limit its memory usage to a specified size. This prevents excessive memory consumption and helps maintain system stability. - **Set `maxmemory-policy` to `allkeys-lru`**: This policy allows Redis to evict the least recently used keys when it reaches the memory limit, ensuring that frequently accessed data remains available while older, less-used data is removed. --- docker-compose.yml | 4 ++++ redis.conf | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 redis.conf diff --git a/docker-compose.yml b/docker-compose.yml index 9e4ae9dd730..b11db2d79aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -110,6 +110,10 @@ services: test: redis-cli ping | grep PONG volumes: - "sentry-redis:/data" + - type: bind + read_only: true + source: ./redis.conf + target: /usr/local/etc/redis/redis.conf ulimits: nofile: soft: 10032 diff --git a/redis.conf b/redis.conf new file mode 100644 index 00000000000..25103a88119 --- /dev/null +++ b/redis.conf @@ -0,0 +1,26 @@ +# redis.conf + +# The 'maxmemory' directive controls the maximum amount of memory Redis is allowed to use. +# Setting 'maxmemory 0' means there is no limit on memory usage, allowing Redis to use as much +# memory as the operating system allows. This is suitable for environments where memory +# constraints are not a concern. +# +# Alternatively, you can specify a limit, such as 'maxmemory 15gb', to restrict Redis to +# using a maximum of 15 gigabytes of memory. +# +# Example: +# maxmemory 0 # Unlimited memory usage +# maxmemory 15gb # Limit memory usage to 15 GB + +maxmemory 0 + +# maxmemory-policy allkeys-lru +# +# This setting determines how Redis evicts keys when it reaches the memory limit. +# 'allkeys-lru' evicts the least recently used keys from all keys stored in Redis, +# allowing frequently accessed data to remain in memory while older data is removed. +# +# Example: +# maxmemory-policy allkeys-lru # Use LRU eviction for all keys + +maxmemory-policy allkeys-lru From 0b0d0c8e541f1f758213e092df72115866455648 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 19 Nov 2024 16:34:41 +0000 Subject: [PATCH 102/287] fix(redis): Use a safer eviction rule (#3432) Follow up to https://github.com/getsentry/self-hosted/pull/3427#issuecomment-2485612688 --- redis.conf | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/redis.conf b/redis.conf index 25103a88119..090b148b15e 100644 --- a/redis.conf +++ b/redis.conf @@ -14,13 +14,14 @@ maxmemory 0 -# maxmemory-policy allkeys-lru -# # This setting determines how Redis evicts keys when it reaches the memory limit. -# 'allkeys-lru' evicts the least recently used keys from all keys stored in Redis, +# `allkeys-lru` evicts the least recently used keys from all keys stored in Redis, # allowing frequently accessed data to remain in memory while older data is removed. -# -# Example: -# maxmemory-policy allkeys-lru # Use LRU eviction for all keys +# That said we use `volatile-lru` as Redis is used both as a cache and processing +# queue in self-hosted Sentry. +# > The volatile-lru and volatile-random policies are mainly useful when you want to +# > use a single Redis instance for both caching and for a set of persistent keys. +# > However, you should consider running two separate Redis instances in a case like +# > this, if possible. -maxmemory-policy allkeys-lru +maxmemory-policy volatile-lru From 99f715461873c70601bc60913c7652e58bff463b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 26 Nov 2024 17:27:18 +0000 Subject: [PATCH 103/287] release: 24.11.1 --- .env | 10 +++++----- CHANGELOG.md | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 58e11832a61..c24a2cf4249 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.11.1 +SNUBA_IMAGE=getsentry/snuba:24.11.1 +RELAY_IMAGE=getsentry/relay:24.11.1 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.11.1 +VROOM_IMAGE=getsentry/vroom:24.11.1 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a21509417..7b9eeb94db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 24.11.1 + +### Various fixes & improvements + +- fix(redis): Use a safer eviction rule (#3432) by @BYK +- feat: add Redis configuration for improved memory management (#3427) by @Hassanzadeh-sd +- build(deps): bump codecov/codecov-action from 4 to 5 (#3429) by @dependabot + ## 24.11.0 ### Various fixes & improvements From be02e0e4000e4e4f494eb778afe66e8378ace210 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 26 Nov 2024 17:56:03 +0000 Subject: [PATCH 104/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index c24a2cf4249..58e11832a61 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.11.1 -SNUBA_IMAGE=getsentry/snuba:24.11.1 -RELAY_IMAGE=getsentry/relay:24.11.1 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.11.1 -VROOM_IMAGE=getsentry/vroom:24.11.1 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 817fde205f8ed7820f5343c059e8e7e40e69842f Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 28 Nov 2024 00:53:07 +0700 Subject: [PATCH 105/287] ref: remove suggested fix (#3446) --- docker-compose.yml | 1 - sentry/sentry.conf.example.py | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b11db2d79aa..0710ee41e18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,6 @@ x-sentry-defaults: &sentry_defaults SENTRY_EVENT_RETENTION_DAYS: SENTRY_MAIL_HOST: SENTRY_MAX_EXTERNAL_SOURCEMAP_SIZE: - OPENAI_API_KEY: volumes: - "sentry-data:/data" - "./sentry:/etc/sentry" diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 28403d69451..27f6ad02c2a 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -335,20 +335,6 @@ def get_internal_network(): # BITBUCKET_CONSUMER_KEY = 'YOUR_BITBUCKET_CONSUMER_KEY' # BITBUCKET_CONSUMER_SECRET = 'YOUR_BITBUCKET_CONSUMER_SECRET' -############################################## -# Suggested Fix Feature / OpenAI Integration # -############################################## - -# See https://docs.sentry.io/product/issues/issue-details/ai-suggested-solution/ -# for more information about the feature. Make sure the OpenAI's privacy policy is -# aligned with your company. - -# Set the OPENAI_API_KEY on the .env or .env.custom file with a valid -# OpenAI API key to turn on the feature. -OPENAI_API_KEY = env("OPENAI_API_KEY", "") - -SENTRY_FEATURES["organizations:open-ai-suggestion"] = bool(OPENAI_API_KEY) - ############################################## # Content Security Policy settings ############################################## From bc0816cda60324e7db7ef5f7838aed30d99820c7 Mon Sep 17 00:00:00 2001 From: niklassc7 <44340628+niklassc7@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:05:05 +0100 Subject: [PATCH 106/287] add sentry/backup.json to gitignore (#3450) The backup-method described in the [documentation](https://develop.sentry.dev/self-hosted/backup/#backup) `./scripts/backup.sh` creates a backup file at `sentry/backup.json` which should be ignored by git. ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 13f8982c056..26d3ff590b1 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ data/ sentry/sentry.conf.py sentry/config.yml sentry/*.bak +sentry/backup.json sentry/enhance-image.sh sentry/requirements.txt relay/credentials.json From 56db0dbbcb920e9dd02d418baab05a39b786ba57 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 30 Nov 2024 17:04:13 +0700 Subject: [PATCH 107/287] chore(issue-template): ask for machine specification and provide link to security policy (#3447) --- .github/ISSUE_TEMPLATE/problem-report.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/problem-report.yml b/.github/ISSUE_TEMPLATE/problem-report.yml index 53ac4ec2acb..1be5d3e4796 100644 --- a/.github/ISSUE_TEMPLATE/problem-report.yml +++ b/.github/ISSUE_TEMPLATE/problem-report.yml @@ -23,7 +23,7 @@ body: id: docker_version attributes: label: Docker Version - placeholder: 20.10.16 ← should look like this + placeholder: 20.10.16 ← should look like this (docker --version) description: | What version of docker are you using to run self-hosted? e.g: (docker --version) @@ -39,6 +39,16 @@ body: e.g: (docker compose version) validations: required: true + - type: checkboxes + id: machine_specification + attributes: + label: Machine Specification + description: Make sure your system meets the [minimum system requirements of Sentry](https://develop.sentry.dev/self-hosted/#required-minimum-system-resources). + options: + - label: My system meets the minimum system requirements of Sentry + required: true + validations: + required: true - type: textarea id: repro attributes: @@ -54,6 +64,8 @@ body: id: expected attributes: label: Expected Result + description: | + What did you expect to happen? validations: required: true - type: textarea @@ -70,7 +82,7 @@ body: - logs output validations: required: true - - type: textarea + - type: input id: event_id attributes: label: Event ID @@ -82,5 +94,7 @@ body: value: |- ## Thanks Check our [triage docs](https://open.sentry.io/triage/) for what to expect next. + + If you're reporting a security issue, please follow our [security policy](https://github.com/getsentry/.github/blob/main/SECURITY.md) instead. validations: required: false From e535c2b4b3dab2a3b92b18e65cb41bf6c8081141 Mon Sep 17 00:00:00 2001 From: Jeffrey Hung <17494876+Jeffreyhung@users.noreply.github.com> Date: Fri, 6 Dec 2024 01:55:28 -0800 Subject: [PATCH 108/287] feat(release): Replace release bot with GH app (#3458) * Replace release bot with GH app * remove unneeded app token --- .github/workflows/release.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4f6156bc1c..7899952fc0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,15 +19,21 @@ jobs: runs-on: ubuntu-latest name: "Release a new version" steps: + - name: Get auth token + id: token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + with: + app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} - uses: actions/checkout@v4 with: - token: ${{ secrets.GH_RELEASE_PAT }} + token: ${{ steps.token.outputs.token }} fetch-depth: 0 - name: Prepare release id: prepare-release uses: getsentry/action-prepare-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GH_RELEASE_PAT }} + GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: version: ${{ github.event.inputs.version }} force: ${{ github.event.inputs.force }} @@ -42,7 +48,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - token: ${{ secrets.GH_RELEASE_PAT }} fetch-depth: 0 - uses: getsentry/action-release@v1 env: From 3834ca7a61b8f5ecbd1d461bf252e5b08afe6f6e Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 6 Dec 2024 10:55:11 +0000 Subject: [PATCH 109/287] fix(redis): Actually use custom config (#3459) Follow up to https://github.com/getsentry/self-hosted/pull/3427#issuecomment-2518128717 where we created and mounted a custom Redis config only to not use it :facepalm: --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 0710ee41e18..eeae00ee981 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,6 +117,7 @@ services: nofile: soft: 10032 hard: 10032 + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] postgres: <<: *restart_policy # Using the same postgres version as Sentry dev for consistency purposes From 078507ba7e64e94998f09ab5808809356856ef83 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 11 Dec 2024 19:36:09 +0000 Subject: [PATCH 110/287] release: 24.11.2 --- .env | 10 +++++----- CHANGELOG.md | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 58e11832a61..cae3f775a6f 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.11.2 +SNUBA_IMAGE=getsentry/snuba:24.11.2 +RELAY_IMAGE=getsentry/relay:24.11.2 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.11.2 +VROOM_IMAGE=getsentry/vroom:24.11.2 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b9eeb94db2..7679ba12e93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 24.11.2 + +### Various fixes & improvements + +- fix(redis): Actually use custom config (#3459) by @BYK +- feat(release): Replace release bot with GH app (#3458) by @Jeffreyhung +- chore(issue-template): ask for machine specification and provide link to security policy (#3447) by @aldy505 +- add sentry/backup.json to gitignore (#3450) by @niklassc7 +- ref: remove suggested fix (#3446) by @aldy505 + ## 24.11.1 ### Various fixes & improvements From 1f63d4d2e2122e5cbf58555c450c9d60e5e7f3bc Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 11 Dec 2024 21:48:37 +0000 Subject: [PATCH 111/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index cae3f775a6f..58e11832a61 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.11.2 -SNUBA_IMAGE=getsentry/snuba:24.11.2 -RELAY_IMAGE=getsentry/relay:24.11.2 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.11.2 -VROOM_IMAGE=getsentry/vroom:24.11.2 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 6ba735824a6fc5780083b1f339c82fade81a98cd Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sun, 15 Dec 2024 18:05:38 +0000 Subject: [PATCH 112/287] release: 24.12.0 --- .env | 10 +++++----- CHANGELOG.md | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 58e11832a61..90d98b2830d 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.12.0 +SNUBA_IMAGE=getsentry/snuba:24.12.0 +RELAY_IMAGE=getsentry/relay:24.12.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.12.0 +VROOM_IMAGE=getsentry/vroom:24.12.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7679ba12e93..c08109c3f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 24.12.0 + +- No documented changes. + ## 24.11.2 ### Various fixes & improvements From 245f3d1a87c1b393b33eda444afc2dfa50e3f3ea Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 17 Dec 2024 23:39:36 +0000 Subject: [PATCH 113/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 90d98b2830d..58e11832a61 100644 --- a/.env +++ b/.env @@ -6,11 +6,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.12.0 -SNUBA_IMAGE=getsentry/snuba:24.12.0 -RELAY_IMAGE=getsentry/relay:24.12.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.12.0 -VROOM_IMAGE=getsentry/vroom:24.12.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 01d7741bc5ec174a5a23befb9ed2f75456890636 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 20 Dec 2024 22:32:29 +0300 Subject: [PATCH 114/287] fix(nginx): _assets should rewrite to _static/sentry/dist (#3483) Our default fallback, `_assets`, assumes we use a CDN which is not the case on self-hosted. This patch adds a stop-gap fix for front-end URLs asking for this path. Should fix #3479 and #3470. --- _integration-test/test_run.py | 8 ++++++++ nginx.conf | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index 2b5774832b9..d1c8f4547c2 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -80,6 +80,14 @@ def test_initial_redirect(): assert initial_auth_redirect.url == f"{SENTRY_TEST_HOST}/auth/login/sentry/" +def test_asset_internal_rewrite(): + """Tests whether we correctly map `/_assets/*` to `/_static/dist/sentry` as + we don't have a CDN setup in self-hosted.""" + response = httpx.get(f"{SENTRY_TEST_HOST}/_assets/entrypoints/app.js") + assert response.status_code == 200 + assert response.headers["Content-Type"] == "text/javascript" + + def test_login(client_login): client, login_response = client_login parser = BeautifulSoup(login_response.text, "html.parser") diff --git a/nginx.conf b/nginx.conf index 32d9487f809..94d7964d58d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -98,6 +98,10 @@ http { location / { proxy_pass http://sentry; } + location /_assets/ { + proxy_pass http://sentry/_static/dist/sentry/; + proxy_hide_header Content-Disposition; + } location /_static/ { proxy_pass http://sentry; proxy_hide_header Content-Disposition; From 92d7d836a33ba83c84780aad4cbf85d643261075 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sun, 22 Dec 2024 03:37:50 +0700 Subject: [PATCH 115/287] chore(relay): provide opt-in max_memory_percent config as workaround for failing healthcheck (#3486) See https://github.com/getsentry/self-hosted/issues/3330 --- relay/config.example.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/relay/config.example.yml b/relay/config.example.yml index 52e6630671f..f73e45acca8 100644 --- a/relay/config.example.yml +++ b/relay/config.example.yml @@ -11,3 +11,10 @@ processing: - {name: "message.max.bytes", value: 50000000} # 50MB redis: redis://redis:6379 geoip_path: "/geoip/GeoLite2-City.mmdb" + +# In some cases, relay might fail to find out the actual machine memory +# therefore it makes the healthcheck fail and events can't be submitted. +# As a workaround, uncomment the following line: +# +# health: +# max_memory_percent: 1.0 From aebe5542d40b2a296dbec46d5dc809ebcf66a761 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sun, 22 Dec 2024 03:59:30 +0700 Subject: [PATCH 116/287] chore: clearer message for errors-only mode (#3487) --- .env | 3 +++ sentry/sentry.conf.example.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 58e11832a61..5688114306b 100644 --- a/.env +++ b/.env @@ -1,4 +1,7 @@ COMPOSE_PROJECT_NAME=sentry-self-hosted +# Set COMPOSE_PROFILES to "feature-complete" to enable all features +# To enable errors monitoring only, set COMPOSE_PROFILES=errors-only +# See https://develop.sentry.dev/self-hosted/experimental/errors-only/ COMPOSE_PROFILES=feature-complete SENTRY_EVENT_RETENTION_DAYS=90 # You can either use a port number or an IP:PORT combo for SENTRY_BIND diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 27f6ad02c2a..98e7a6c0fb3 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -72,6 +72,18 @@ def get_internal_network(): env("SENTRY_EVENT_RETENTION_DAYS", "90") ) +# Self-hosted Sentry infamously has a lot of Docker containers required to make +# all the features work. Oftentimes, users don't use the full feature set that +# requires all the containers. This is a way to enable only the error monitoring +# feature which also reduces the amount of containers required to run Sentry. +# +# To make Sentry work with all features, set `COMPOSE_PROFILES` to `feature-complete` +# in your `.env` file. To enable only the error monitoring feature, set +# `COMPOSE_PROFILES` to `errors-only`. +# +# See https://develop.sentry.dev/self-hosted/experimental/errors-only/ +SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete" + ######### # Redis # ######### @@ -373,10 +385,6 @@ def get_internal_network(): # if you're using it directly like a CDN instead of using the loader script. JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle%s.min.js" - -# If you would like to use self-hosted Sentry with only errors enabled, please set this -SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete" - ##################### # Insights Settings # ##################### From d8f64c6e8e4e63b50367a5927c844b172ecbfb54 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sat, 21 Dec 2024 21:02:30 +0000 Subject: [PATCH 117/287] release: 24.12.1 --- .env | 10 +++++----- CHANGELOG.md | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5688114306b..c17453c64bb 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:24.12.1 +SNUBA_IMAGE=getsentry/snuba:24.12.1 +RELAY_IMAGE=getsentry/relay:24.12.1 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.12.1 +VROOM_IMAGE=getsentry/vroom:24.12.1 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index c08109c3f9e..56d9cfebee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 24.12.1 + +### Various fixes & improvements + +- chore: clearer message for errors-only mode (#3487) by @aldy505 +- chore(relay): provide opt-in max_memory_percent config as workaround for failing healthcheck (#3486) by @aldy505 +- fix(nginx): _assets should rewrite to _static/sentry/dist (#3483) by @BYK + ## 24.12.0 - No documented changes. From 4fa0833fd76537d3f7c09765f80e5ef82c9bc0f4 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sat, 21 Dec 2024 21:26:04 +0000 Subject: [PATCH 118/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index c17453c64bb..5688114306b 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:24.12.1 -SNUBA_IMAGE=getsentry/snuba:24.12.1 -RELAY_IMAGE=getsentry/relay:24.12.1 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:24.12.1 -VROOM_IMAGE=getsentry/vroom:24.12.1 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 0ca9311955a8b8d84d38b12385e2e252e2a73124 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 08:41:27 -0800 Subject: [PATCH 119/287] build(deps): bump actions/create-github-app-token from 1.11.0 to 1.11.1 (#3492) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.0 to 1.11.1. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/5d869da34e18e7287c1daad50e0b8ea0f506ce69...c1a285145b9d317df6ced56c09f525b5c2b6f755) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7899952fc0f..38e929334f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From ad39dabdf08fc550d4ed8e79c2dd590ce76d0016 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 24 Dec 2024 01:32:17 +0300 Subject: [PATCH 120/287] ref(geoip): Remove geoipupdate from compose (#3490) `geoipupdate` is not used by any other service nor it is needed for any service to run. Moreover, it is a one-shot command, causing `docker compose up --wait` to fail when it exits with a non-zero status. This happens when one has not yet set up their credentials and they may choose to never do this. This PR removes `geoipupdate` from the `docker-compose.yml` file and moves the command directly into the geoip related script. One may run this whenever they want to update their GeoIP database. This PR needs an accompanying docs change. --- .github/workflows/test.yml | 1 + docker-compose.yml | 9 --------- install/geoip.sh | 2 +- install/set-up-and-migrate-database.sh | 8 +------- install/upgrade-clickhouse.sh | 20 +++----------------- install/upgrade-postgres.sh | 2 +- install/wrap-up.sh | 8 ++++---- sentry-admin.sh | 2 +- 8 files changed, 12 insertions(+), 40 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 291e78c7c8f..7660d437d3d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -134,6 +134,7 @@ jobs: - name: Integration Test run: | + docker compose up --wait if [ "${{ matrix.compose_version }}" = "v2.19.0" ]; then pytest --reruns 3 --cov --junitxml=junit.xml _integration-test/ --customizations=${{ matrix.customizations }} else diff --git a/docker-compose.yml b/docker-compose.yml index eeae00ee981..9d8ee5a9e4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -207,15 +207,6 @@ services: interval: 10s timeout: 10s retries: 30 - geoipupdate: - image: "ghcr.io/maxmind/geoipupdate:v6.1.0" - # Override the entrypoint in order to avoid using envvars for config. - # Futz with settings so we can keep mmdb and conf in same dir on host - # (image looks for them in separate dirs by default). - entrypoint: - ["/usr/bin/geoipupdate", "-d", "/sentry", "-f", "/sentry/GeoIP.conf"] - volumes: - - "./geoip:/sentry" snuba-api: <<: *snuba_defaults # Kafka consumer responsible for feeding events into Clickhouse diff --git a/install/geoip.sh b/install/geoip.sh index 577b6ba5ca5..041db9b6833 100644 --- a/install/geoip.sh +++ b/install/geoip.sh @@ -21,7 +21,7 @@ install_geoip() { else echo "IP address geolocation is configured for updates." echo "Updating IP address geolocation database ... " - if ! $dcr geoipupdate; then + if ! docker run --rm -v "./geoip:/sentry" --entrypoint '/usr/bin/geoipupdate' "ghcr.io/maxmind/geoipupdate:v6.1.0" "-d" "/sentry" "-f" "/sentry/GeoIP.conf"; then result='Error' fi echo "$result updating IP address geolocation database." diff --git a/install/set-up-and-migrate-database.sh b/install/set-up-and-migrate-database.sh index 7bf74f40e95..770bfbdc61b 100644 --- a/install/set-up-and-migrate-database.sh +++ b/install/set-up-and-migrate-database.sh @@ -1,13 +1,7 @@ echo "${_group}Setting up / migrating database ..." # Fixes https://github.com/getsentry/self-hosted/issues/2758, where a migration fails due to indexing issue -$dc up -d postgres -# Wait for postgres -RETRIES=5 -until $dc exec postgres psql -U postgres -c "select 1" >/dev/null 2>&1 || [ $RETRIES -eq 0 ]; do - echo "Waiting for postgres server, $((RETRIES--)) remaining attempts..." - sleep 1 -done +$dc up --wait postgres os=$($dc exec postgres cat /etc/os-release | grep 'ID=debian') if [[ -z $os ]]; then diff --git a/install/upgrade-clickhouse.sh b/install/upgrade-clickhouse.sh index e9472384009..05e74bb00b9 100644 --- a/install/upgrade-clickhouse.sh +++ b/install/upgrade-clickhouse.sh @@ -1,33 +1,19 @@ echo "${_group}Upgrading Clickhouse ..." -function wait_for_clickhouse() { - # Wait for clickhouse - RETRIES=30 - until $dc ps clickhouse | grep 'healthy' || [ $RETRIES -eq 0 ]; do - echo "Waiting for clickhouse server, $((RETRIES--)) remaining attempts..." - sleep 1 - done -} - # First check to see if user is upgrading by checking for existing clickhouse volume if [[ -n "$(docker volume ls -q --filter name=sentry-clickhouse)" ]]; then # Start clickhouse if it is not already running - $dc up -d clickhouse - - # Wait for clickhouse - wait_for_clickhouse + $dc up --wait clickhouse # In order to get to 23.8, we need to first upgrade go from 21.8 -> 22.8 -> 23.3 -> 23.8 version=$($dc exec clickhouse clickhouse-client -q 'SELECT version()') if [[ "$version" == "21.8.13.1.altinitystable" || "$version" == "21.8.12.29.altinitydev.arm" ]]; then $dc down clickhouse $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:22.8.15.25.altinitystable clickhouse - $dc up -d clickhouse - wait_for_clickhouse + $dc up --wait clickhouse $dc down clickhouse $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:23.3.19.33.altinitystable clickhouse - $dc up -d clickhouse - wait_for_clickhouse + $dc up --wait clickhouse else echo "Detected clickhouse version $version. Skipping upgrades!" fi diff --git a/install/upgrade-postgres.sh b/install/upgrade-postgres.sh index 86b76646f33..fa66a0aa4ab 100644 --- a/install/upgrade-postgres.sh +++ b/install/upgrade-postgres.sh @@ -20,7 +20,7 @@ if [[ -n "$(docker volume ls -q --filter name=sentry-postgres)" && "$(docker run docker volume rm sentry-postgres-new echo "Re-indexing due to glibc change, this may take a while..." echo "Starting up new PostgreSQL version" - $dc up -d postgres + $dc up --wait postgres # Wait for postgres RETRIES=5 diff --git a/install/wrap-up.sh b/install/wrap-up.sh index 8840262f25d..c301c823f2b 100644 --- a/install/wrap-up.sh +++ b/install/wrap-up.sh @@ -2,7 +2,7 @@ if [[ "$MINIMIZE_DOWNTIME" ]]; then echo "${_group}Waiting for Sentry to start ..." # Start the whole setup, except nginx and relay. - $dc up -d --remove-orphans $($dc config --services | grep -v -E '^(nginx|relay)$') + $dc up --wait --remove-orphans $($dc config --services | grep -v -E '^(nginx|relay)$') $dc restart relay $dc exec -T nginx nginx -s reload @@ -10,7 +10,7 @@ if [[ "$MINIMIZE_DOWNTIME" ]]; then -c 'while [[ "$(wget -T 1 -q -O- http://web:9000/_health/)" != "ok" ]]; do sleep 0.5; done' # Make sure everything is up. This should only touch relay and nginx - $dc up -d + $dc up --wait echo "${_endgroup}" else @@ -20,9 +20,9 @@ else echo "You're all done! Run the following command to get Sentry running:" echo "" if [[ "${_ENV}" =~ ".env.custom" ]]; then - echo " $dc_base --env-file ${_ENV} up -d" + echo " $dc_base --env-file ${_ENV} up --wait" else - echo " $dc_base up -d" + echo " $dc_base up --wait" fi echo "" echo "-----------------------------------------------------------------" diff --git a/sentry-admin.sh b/sentry-admin.sh index d775e905e94..3db5f2ed044 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -22,7 +22,7 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { - $dc run -v "$VOLUME_MAPPING" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 + $dcr --quiet-pull -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 } # Function to modify lines starting with `Usage: sentry` to say `Usage: ./sentry-admin.sh` instead. From 282410abff0a117e10822d9d84622356b5c3933e Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 24 Dec 2024 02:05:22 +0300 Subject: [PATCH 121/287] ref(snuba): Combine bootstrap & migrate for faster bootstrap (#3491) I think we split these actions in the past due to some lack of options for them to work together properly. Right now looks like `bootstrap` would automatically migrate and propagates the `force` flag. --- install/bootstrap-snuba.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/install/bootstrap-snuba.sh b/install/bootstrap-snuba.sh index 2952ed0b33c..489c4da0e17 100644 --- a/install/bootstrap-snuba.sh +++ b/install/bootstrap-snuba.sh @@ -1,6 +1,5 @@ echo "${_group}Bootstrapping and migrating Snuba ..." -$dcr snuba-api bootstrap --no-migrate --force -$dcr snuba-api migrations migrate --force +$dcr snuba-api bootstrap --force echo "${_endgroup}" From 1bb22c032d0baf7c4f6d935aa3559a5ccf91ba40 Mon Sep 17 00:00:00 2001 From: Mohamed Elneily Date: Sun, 29 Dec 2024 20:19:04 +0200 Subject: [PATCH 122/287] fix: Remove the extra space in the log file names (#3212) Update `_lib.sh` to remove the extra space in the log file name. This fixes the log files name not being included in `.gitinore` --- scripts/_lib.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/_lib.sh b/scripts/_lib.sh index 7ec51eecac1..a742f8acc51 100755 --- a/scripts/_lib.sh +++ b/scripts/_lib.sh @@ -76,7 +76,7 @@ MINIMIZE_DOWNTIME="${MINIMIZE_DOWNTIME:-}" STOP_TIMEOUT=60 # Save logs in order to send envelope to Sentry -log_file=sentry_"$cmd"_log-$(date +'%Y-%m-%d_%H-%M-%S').txt +log_file=sentry_"${cmd%% *}"_log-$(date +'%Y-%m-%d_%H-%M-%S').txt exec &> >(tee -a "$log_file") version="" From d5b49a41362f81caccbbbdc079d0904d352312ee Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 31 Dec 2024 01:37:53 +0300 Subject: [PATCH 123/287] ci: Cache postgres volume after first migration (#3488) This patch caches all DB volumes based on the sentry and snuba images to avoid doing the same migrations over and over for every test run. This shaved off a whole minute from "Install self-hosted" jobs (so ~20% speed increase). Left side: cached re-run -- Right side: no-cache initial run ![image](https://github.com/user-attachments/assets/55b923ea-d4c8-44bf-ba3e-0d5708781fd8) --- .github/workflows/test.yml | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7660d437d3d..36a54676073 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,11 @@ on: pull_request: schedule: - cron: "0 0,12 * * *" + +concurrency: + group: ${{ github.ref_name || github.sha }} + cancel-in-progress: true + defaults: run: shell: bash @@ -66,9 +71,48 @@ jobs: sudo curl -L https://github.com/docker/compose/releases/download/v2.26.0/docker-compose-`uname -s`-`uname -m` -o "/usr/local/lib/docker/cli-plugins/docker-compose" sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" + - name: Prepare Docker Volume Caching + id: cache_key + run: | + # Set permissions for docker volumes so we can cache and restore + sudo chmod o+x /var/lib/docker + sudo chmod -R o+rwx /var/lib/docker/volumes + source .env + SENTRY_IMAGE_SHA=$(docker buildx imagetools inspect $SENTRY_IMAGE --format "{{println .Manifest.Digest}}") + echo "SENTRY_IMAGE_SHA=$SENTRY_IMAGE_SHA" >> $GITHUB_OUTPUT + SNUBA_IMAGE_SHA=$(docker buildx imagetools inspect $SNUBA_IMAGE --format "{{println .Manifest.Digest}}") + echo "SNUBA_IMAGE_SHA=$SNUBA_IMAGE_SHA" >> $GITHUB_OUTPUT + + - name: Restore DB Volumes Cache + id: restore_cache + uses: actions/cache/restore@v4 + with: + key: db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }}-${{ steps.cache_key.outputs.SNUBA_IMAGE_SHA }} + restore-keys: | + db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }} + db-volumes-v4- + path: | + /var/lib/docker/volumes/sentry-postgres/_data + /var/lib/docker/volumes/sentry-clickhouse/_data + - name: Install ${{ env.LATEST_TAG }} run: ./install.sh + - name: Prepare Docker Volume Caching + run: | + # Set permissions for docker volumes so we can cache and restore + sudo chmod o+x /var/lib/docker + sudo chmod -R o+rx /var/lib/docker/volumes + + - name: Save DB Volumes Cache + if: steps.restore_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + key: ${{ steps.restore_cache.outputs.cache-primary-key }} + path: | + /var/lib/docker/volumes/sentry-postgres/_data + /var/lib/docker/volumes/sentry-clickhouse/_data + - name: Checkout current ref uses: actions/checkout@v4 @@ -125,6 +169,30 @@ jobs: sudo curl -L https://github.com/docker/compose/releases/download/${{ matrix.compose_version }}/docker-compose-`uname -s`-`uname -m` -o "${{ matrix.compose_path }}/docker-compose" sudo chmod +x "${{ matrix.compose_path }}/docker-compose" + - name: Prepare Docker Volume Caching + id: cache_key + run: | + # Set permissions for docker volumes so we can cache and restore + sudo chmod o+x /var/lib/docker + sudo chmod -R o+rwx /var/lib/docker/volumes + source .env + SENTRY_IMAGE_SHA=$(docker buildx imagetools inspect $SENTRY_IMAGE --format "{{println .Manifest.Digest}}") + echo "SENTRY_IMAGE_SHA=$SENTRY_IMAGE_SHA" >> $GITHUB_OUTPUT + SNUBA_IMAGE_SHA=$(docker buildx imagetools inspect $SNUBA_IMAGE --format "{{println .Manifest.Digest}}") + echo "SNUBA_IMAGE_SHA=$SNUBA_IMAGE_SHA" >> $GITHUB_OUTPUT + + - name: Restore DB Volumes Cache + id: restore_cache + uses: actions/cache/restore@v4 + with: + key: db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }}-${{ steps.cache_key.outputs.SNUBA_IMAGE_SHA }} + restore-keys: | + db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }} + db-volumes-v4- + path: | + /var/lib/docker/volumes/sentry-postgres/_data + /var/lib/docker/volumes/sentry-clickhouse/_data + - name: Install self-hosted uses: nick-fields/retry@v3 with: @@ -132,6 +200,21 @@ jobs: max_attempts: 3 command: ./install.sh + - name: Prepare Docker Volume Caching + run: | + # Set permissions for docker volumes so we can cache and restore + sudo chmod o+x /var/lib/docker + sudo chmod -R o+rx /var/lib/docker/volumes + + - name: Save DB Volumes Cache + if: steps.restore_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + key: ${{ steps.restore_cache.outputs.cache-primary-key }} + path: | + /var/lib/docker/volumes/sentry-postgres/_data + /var/lib/docker/volumes/sentry-clickhouse/_data + - name: Integration Test run: | docker compose up --wait From 8653327bc1f2097ad8666f6b87da31675f5b94f7 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 31 Dec 2024 10:53:14 -0800 Subject: [PATCH 124/287] chore: Remove everything zookeeper (#3499) --- install.sh | 1 - install/update-docker-volume-permissions.sh | 9 --------- install/wrap-up.sh | 5 ----- 3 files changed, 15 deletions(-) delete mode 100644 install/update-docker-volume-permissions.sh diff --git a/install.sh b/install.sh index e9460d36ceb..23726ce97fd 100755 --- a/install.sh +++ b/install.sh @@ -24,7 +24,6 @@ source install/check-minimum-requirements.sh # in order to determine whether or not the clickhouse version needs to be upgraded. source install/upgrade-clickhouse.sh source install/turn-things-off.sh -source install/update-docker-volume-permissions.sh source install/create-docker-volumes.sh source install/ensure-files-from-examples.sh source install/check-memcached-backend.sh diff --git a/install/update-docker-volume-permissions.sh b/install/update-docker-volume-permissions.sh deleted file mode 100644 index 8ac0be400c4..00000000000 --- a/install/update-docker-volume-permissions.sh +++ /dev/null @@ -1,9 +0,0 @@ -echo "${_group}Ensuring Kafka and Zookeeper volumes have correct permissions ..." - -# Only supporting platforms on linux x86 platforms and not apple silicon. I'm assuming that folks using apple silicon are doing it for dev purposes and it's difficult -# to change permissions of docker volumes since it is run in a VM. -if [[ -n "$(docker volume ls -q -f name=sentry-zookeeper)" && -n "$(docker volume ls -q -f name=sentry-kafka)" ]]; then - docker run --rm -v "sentry-zookeeper:/sentry-zookeeper-data" -v "sentry-kafka:/sentry-kafka-data" -v "${COMPOSE_PROJECT_NAME}_sentry-zookeeper-log:/sentry-zookeeper-log-data" busybox chmod -R a+w /sentry-zookeeper-data /sentry-kafka-data /sentry-zookeeper-log-data -fi - -echo "${_endgroup}" diff --git a/install/wrap-up.sh b/install/wrap-up.sh index c301c823f2b..dcc4e33c218 100644 --- a/install/wrap-up.sh +++ b/install/wrap-up.sh @@ -28,8 +28,3 @@ else echo "-----------------------------------------------------------------" echo "" fi - -# TODO(getsentry/self-hosted#2489) -if docker volume ls | grep -qw sentry-zookeeper; then - docker volume rm sentry-zookeeper -fi From 8c1653dc4a4b0b2b8c8fa81f668bcc52d9201f2b Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Wed, 1 Jan 2025 00:26:22 +0300 Subject: [PATCH 125/287] ci: Skip DB ops during install completely on cache hit (#3496) Follow up to #3488 A new record: 2m 8s for installing self-hosted: ![image](https://github.com/user-attachments/assets/7cc6409d-5388-49ba-ad87-b7a1e99c9acc) --- .github/workflows/test.yml | 39 +++++++++++++++++++----- install/set-up-and-migrate-database.sh | 41 ++++++++++++++------------ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36a54676073..79f8dc5a2ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,15 +94,24 @@ jobs: path: | /var/lib/docker/volumes/sentry-postgres/_data /var/lib/docker/volumes/sentry-clickhouse/_data + /var/lib/docker/volumes/sentry-kafka/_data - name: Install ${{ env.LATEST_TAG }} - run: ./install.sh + env: + SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} + run: | + # This is for the cache restore on Kafka to work in older releases + docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data + ./install.sh - name: Prepare Docker Volume Caching run: | # Set permissions for docker volumes so we can cache and restore sudo chmod o+x /var/lib/docker sudo chmod -R o+rx /var/lib/docker/volumes + # Set tar ownership for it to be able to read + # From: https://github.com/actions/toolkit/issues/946#issuecomment-1726311681 + sudo chown root /usr/bin/tar && sudo chmod u+s /usr/bin/tar - name: Save DB Volumes Cache if: steps.restore_cache.outputs.cache-hit != 'true' @@ -112,12 +121,22 @@ jobs: path: | /var/lib/docker/volumes/sentry-postgres/_data /var/lib/docker/volumes/sentry-clickhouse/_data + /var/lib/docker/volumes/sentry-kafka/_data - name: Checkout current ref uses: actions/checkout@v4 - name: Install current ref - run: ./install.sh + run: | + # This is for the cache restore on Kafka to work in older releases + docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data + ./install.sh + + - name: Inspect failure + if: failure() + run: | + docker compose ps + docker compose logs integration-test: if: github.repository_owner == 'getsentry' @@ -192,19 +211,24 @@ jobs: path: | /var/lib/docker/volumes/sentry-postgres/_data /var/lib/docker/volumes/sentry-clickhouse/_data + /var/lib/docker/volumes/sentry-kafka/_data - name: Install self-hosted - uses: nick-fields/retry@v3 - with: - timeout_minutes: 10 - max_attempts: 3 - command: ./install.sh + env: + SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} + run: | + # This is for the cache restore on Kafka to work in older releases + docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data + ./install.sh - name: Prepare Docker Volume Caching run: | # Set permissions for docker volumes so we can cache and restore sudo chmod o+x /var/lib/docker sudo chmod -R o+rx /var/lib/docker/volumes + # Set tar ownership for it to be able to read + # From: https://github.com/actions/toolkit/issues/946#issuecomment-1726311681 + sudo chown root /usr/bin/tar && sudo chmod u+s /usr/bin/tar - name: Save DB Volumes Cache if: steps.restore_cache.outputs.cache-hit != 'true' @@ -214,6 +238,7 @@ jobs: path: | /var/lib/docker/volumes/sentry-postgres/_data /var/lib/docker/volumes/sentry-clickhouse/_data + /var/lib/docker/volumes/sentry-kafka/_data - name: Integration Test run: | diff --git a/install/set-up-and-migrate-database.sh b/install/set-up-and-migrate-database.sh index 770bfbdc61b..a1cc8323e89 100644 --- a/install/set-up-and-migrate-database.sh +++ b/install/set-up-and-migrate-database.sh @@ -1,16 +1,17 @@ echo "${_group}Setting up / migrating database ..." -# Fixes https://github.com/getsentry/self-hosted/issues/2758, where a migration fails due to indexing issue -$dc up --wait postgres +if [[ -z "${SKIP_DB_MIGRATIONS:-}" ]]; then + # Fixes https://github.com/getsentry/self-hosted/issues/2758, where a migration fails due to indexing issue + $dc up --wait postgres -os=$($dc exec postgres cat /etc/os-release | grep 'ID=debian') -if [[ -z $os ]]; then - echo "Postgres image debian check failed, exiting..." - exit 1 -fi + os=$($dc exec postgres cat /etc/os-release | grep 'ID=debian') + if [[ -z $os ]]; then + echo "Postgres image debian check failed, exiting..." + exit 1 + fi -# Using django ORM to provide broader support for users with external databases -$dcr web shell -c " + # Using django ORM to provide broader support for users with external databases + $dcr web shell -c " from django.db import connection with connection.cursor() as cursor: @@ -18,16 +19,18 @@ with connection.cursor() as cursor: cursor.execute('DROP INDEX IF EXISTS sentry_groupedmessage_project_id_id_515aaa7e_uniq;') " -if [[ -n "${CI:-}" || "${SKIP_USER_CREATION:-0}" == 1 ]]; then - $dcr web upgrade --noinput --create-kafka-topics - echo "" - echo "Did not prompt for user creation. Run the following command to create one" - echo "yourself (recommended):" - echo "" - echo " $dc_base run --rm web createuser" - echo "" + if [[ -n "${CI:-}" || "${SKIP_USER_CREATION:-0}" == 1 ]]; then + $dcr web upgrade --noinput --create-kafka-topics + echo "" + echo "Did not prompt for user creation. Run the following command to create one" + echo "yourself (recommended):" + echo "" + echo " $dc_base run --rm web createuser" + echo "" + else + $dcr web upgrade --create-kafka-topics + fi else - $dcr web upgrade --create-kafka-topics + echo "Skipped DB migrations due to SKIP_DB_MIGRATIONS=$SKIP_DB_MIGRATIONS" fi - echo "${_endgroup}" From cb9e0ce5526fe59f07758ecbca4252c7a56a89c4 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 3 Jan 2025 00:36:33 +0300 Subject: [PATCH 126/287] ci: Only test on compose 2.26 w/ customizations (#3506) Docker Compose is much more robust nowadays compared to the past where we had to maintain tests for both v1 and v2 and then a specific version of v2. Hence, we are removing tests for the older versions of Docker Compose with this patch. We also remove the separate tests for customizations and w/o customizations as the one with customizations should cover the one without them anyway. This reduces the CI workload to 25% of what it was --- .github/workflows/test.yml | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79f8dc5a2ae..cbea11a9311 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -141,19 +141,8 @@ jobs: integration-test: if: github.repository_owner == 'getsentry' runs-on: ubuntu-22.04 - name: integration test ${{ matrix.compose_version }} - customizations ${{ matrix.customizations }} - strategy: - fail-fast: false - matrix: - customizations: ["disabled", "enabled"] - compose_version: ["v2.19.0", "v2.26.0"] - include: - - compose_version: "v2.19.0" - compose_path: "/usr/local/lib/docker/cli-plugins" - - compose_version: "v2.26.0" - compose_path: "/usr/local/lib/docker/cli-plugins" + name: integration test env: - COMPOSE_PROJECT_NAME: self-hosted-${{ strategy.job-index }} REPORT_SELF_HOSTED_ISSUES: 0 SELF_HOSTED_TESTING_DSN: ${{ vars.SELF_HOSTED_TESTING_DSN }} steps: @@ -177,16 +166,19 @@ jobs: fi - name: Get Compose + env: + COMPOSE_PATH: /usr/local/lib/docker/cli-plugins + COMPOSE_VERSION: 'v2.26.0' run: | # Always remove `docker compose` support as that's the newer version # and comes installed by default nowadays. sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" # Docker Compose v1 is installed here, remove it sudo rm -f "/usr/local/bin/docker-compose" - sudo rm -f "${{ matrix.compose_path }}/docker-compose" - sudo mkdir -p "${{ matrix.compose_path }}" - sudo curl -L https://github.com/docker/compose/releases/download/${{ matrix.compose_version }}/docker-compose-`uname -s`-`uname -m` -o "${{ matrix.compose_path }}/docker-compose" - sudo chmod +x "${{ matrix.compose_path }}/docker-compose" + sudo rm -f "${{ env.COMPOSE_PATH }}/docker-compose" + sudo mkdir -p "${{ env.COMPOSE_PATH }}" + sudo curl -L https://github.com/docker/compose/releases/download/${{ env.COMPOSE_VERSION }}/docker-compose-`uname -s`-`uname -m` -o "${{ env.COMPOSE_PATH }}/docker-compose" + sudo chmod +x "${{ env.COMPOSE_PATH }}/docker-compose" - name: Prepare Docker Volume Caching id: cache_key @@ -243,11 +235,7 @@ jobs: - name: Integration Test run: | docker compose up --wait - if [ "${{ matrix.compose_version }}" = "v2.19.0" ]; then - pytest --reruns 3 --cov --junitxml=junit.xml _integration-test/ --customizations=${{ matrix.customizations }} - else - pytest --cov --junitxml=junit.xml _integration-test/ --customizations=${{ matrix.customizations }} - fi + pytest --cov --junitxml=junit.xml _integration-test/ --customizations=enabled - name: Inspect failure if: failure() From 63334cbcc23132e4d6832809ed399c75f48e4683 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 10 Jan 2025 20:51:11 +0000 Subject: [PATCH 127/287] ci: Move e2e test action into the repo (#3519) This is an initial transitionary patch before landing #3516. Once we land this, we will update users of the old action to use this one and remove that repo. Then land #3516 safely. Great thing is, with this patch and the subsequent update to getsentry/action-self-hosted-e2e-tests to use this one, all the repos would be using the Docker Volume caching we introduced in #3488. --- .github/workflows/test.yml | 117 +---------------------------- action.yaml | 149 +++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 113 deletions(-) create mode 100644 action.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cbea11a9311..4dff1aa5fee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,21 +19,6 @@ defaults: run: shell: bash jobs: - e2e-test: - if: github.repository_owner == 'getsentry' - runs-on: ubuntu-22.04 - name: "Sentry self-hosted end-to-end tests" - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - path: self-hosted - - - name: End to end tests - uses: getsentry/action-self-hosted-e2e-tests@main - with: - project_name: self-hosted - unit-test: if: github.repository_owner == 'getsentry' runs-on: ubuntu-22.04 @@ -107,6 +92,7 @@ jobs: - name: Prepare Docker Volume Caching run: | # Set permissions for docker volumes so we can cache and restore + # We need these for the backup/restore test snapshotting too sudo chmod o+x /var/lib/docker sudo chmod -R o+rx /var/lib/docker/volumes # Set tar ownership for it to be able to read @@ -149,108 +135,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Setup dev environment - run: | - pip install -r requirements-dev.txt - echo "PY_COLORS=1" >> "$GITHUB_ENV" - ### pytest-sentry configuration ### - if [ "$GITHUB_REPOSITORY" = "getsentry/self-hosted" ]; then - echo "PYTEST_SENTRY_DSN=$SELF_HOSTED_TESTING_DSN" >> $GITHUB_ENV - echo "PYTEST_SENTRY_TRACES_SAMPLE_RATE=0" >> $GITHUB_ENV - - # This records failures on master to sentry in order to detect flakey tests, as it's - # expected that people have failing tests on their PRs - if [ "$GITHUB_REF" = "refs/heads/master" ]; then - echo "PYTEST_SENTRY_ALWAYS_REPORT=1" >> $GITHUB_ENV - fi - fi - - - name: Get Compose - env: - COMPOSE_PATH: /usr/local/lib/docker/cli-plugins - COMPOSE_VERSION: 'v2.26.0' - run: | - # Always remove `docker compose` support as that's the newer version - # and comes installed by default nowadays. - sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" - # Docker Compose v1 is installed here, remove it - sudo rm -f "/usr/local/bin/docker-compose" - sudo rm -f "${{ env.COMPOSE_PATH }}/docker-compose" - sudo mkdir -p "${{ env.COMPOSE_PATH }}" - sudo curl -L https://github.com/docker/compose/releases/download/${{ env.COMPOSE_VERSION }}/docker-compose-`uname -s`-`uname -m` -o "${{ env.COMPOSE_PATH }}/docker-compose" - sudo chmod +x "${{ env.COMPOSE_PATH }}/docker-compose" - - - name: Prepare Docker Volume Caching - id: cache_key - run: | - # Set permissions for docker volumes so we can cache and restore - sudo chmod o+x /var/lib/docker - sudo chmod -R o+rwx /var/lib/docker/volumes - source .env - SENTRY_IMAGE_SHA=$(docker buildx imagetools inspect $SENTRY_IMAGE --format "{{println .Manifest.Digest}}") - echo "SENTRY_IMAGE_SHA=$SENTRY_IMAGE_SHA" >> $GITHUB_OUTPUT - SNUBA_IMAGE_SHA=$(docker buildx imagetools inspect $SNUBA_IMAGE --format "{{println .Manifest.Digest}}") - echo "SNUBA_IMAGE_SHA=$SNUBA_IMAGE_SHA" >> $GITHUB_OUTPUT - - - name: Restore DB Volumes Cache - id: restore_cache - uses: actions/cache/restore@v4 - with: - key: db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }}-${{ steps.cache_key.outputs.SNUBA_IMAGE_SHA }} - restore-keys: | - db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }} - db-volumes-v4- - path: | - /var/lib/docker/volumes/sentry-postgres/_data - /var/lib/docker/volumes/sentry-clickhouse/_data - /var/lib/docker/volumes/sentry-kafka/_data - - - name: Install self-hosted - env: - SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} - run: | - # This is for the cache restore on Kafka to work in older releases - docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data - ./install.sh - - - name: Prepare Docker Volume Caching - run: | - # Set permissions for docker volumes so we can cache and restore - sudo chmod o+x /var/lib/docker - sudo chmod -R o+rx /var/lib/docker/volumes - # Set tar ownership for it to be able to read - # From: https://github.com/actions/toolkit/issues/946#issuecomment-1726311681 - sudo chown root /usr/bin/tar && sudo chmod u+s /usr/bin/tar - - - name: Save DB Volumes Cache - if: steps.restore_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + - name: Use action from local checkout + uses: './' with: - key: ${{ steps.restore_cache.outputs.cache-primary-key }} - path: | - /var/lib/docker/volumes/sentry-postgres/_data - /var/lib/docker/volumes/sentry-clickhouse/_data - /var/lib/docker/volumes/sentry-kafka/_data - - - name: Integration Test - run: | - docker compose up --wait - pytest --cov --junitxml=junit.xml _integration-test/ --customizations=enabled + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Inspect failure if: failure() run: | docker compose ps docker compose logs - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - slug: getsentry/self-hosted - - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/test-results-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} diff --git a/action.yaml b/action.yaml new file mode 100644 index 00000000000..537be8c8260 --- /dev/null +++ b/action.yaml @@ -0,0 +1,149 @@ +name: "Sentry self-hosted end-to-end tests" +inputs: + project_name: + required: false + description: "e.g. snuba, sentry, relay, self-hosted" + image_url: + required: false + description: "The URL to the built relay, snuba, sentry image to test against." + CODECOV_TOKEN: + required: false + description: "The Codecov token to upload coverage." + +runs: + using: "composite" + steps: + - name: Go into self-hosted directory + shell: bash + run: cd ${{ github.action_path }} + + - name: Configure to use the test image + if: inputs.project_name && inputs.image_url + shell: bash + run: | + image_var=$(echo ${{ inputs.project_name }}_IMAGE | tr '[:lower:]' '[:upper:]') + echo "${image_var}=${{ inputs.image_url }}" >> $GITHUB_ENV + + - name: Setup dev environment + shell: bash + run: | + pip install -r requirements-dev.txt + echo "PY_COLORS=1" >> "$GITHUB_ENV" + ### pytest-sentry configuration ### + if [ "$GITHUB_REPOSITORY" = "getsentry/self-hosted" ]; then + echo "PYTEST_SENTRY_DSN=$SELF_HOSTED_TESTING_DSN" >> $GITHUB_ENV + echo "PYTEST_SENTRY_TRACES_SAMPLE_RATE=0" >> $GITHUB_ENV + + # This records failures on master to sentry in order to detect flakey tests, as it's + # expected that people have failing tests on their PRs + if [ "$GITHUB_REF" = "refs/heads/master" ]; then + echo "PYTEST_SENTRY_ALWAYS_REPORT=1" >> $GITHUB_ENV + fi + fi + + - name: Get Compose + env: + COMPOSE_PATH: /usr/local/lib/docker/cli-plugins + COMPOSE_VERSION: "v2.26.0" + shell: bash + run: | + # Always remove `docker compose` support as that's the newer version + # and comes installed by default nowadays. + sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" + # Docker Compose v1 is installed here, remove it + sudo rm -f "/usr/local/bin/docker-compose" + sudo rm -f "${{ env.COMPOSE_PATH }}/docker-compose" + sudo mkdir -p "${{ env.COMPOSE_PATH }}" + sudo curl -L https://github.com/docker/compose/releases/download/${{ env.COMPOSE_VERSION }}/docker-compose-`uname -s`-`uname -m` -o "${{ env.COMPOSE_PATH }}/docker-compose" + sudo chmod +x "${{ env.COMPOSE_PATH }}/docker-compose" + + - name: Prepare Docker Volume Caching + id: cache_key + shell: bash + run: | + # Set permissions for docker volumes so we can cache and restore + sudo chmod o+x /var/lib/docker + sudo chmod -R o+rwx /var/lib/docker/volumes + source .env + SENTRY_IMAGE_SHA=$(docker buildx imagetools inspect $SENTRY_IMAGE --format "{{println .Manifest.Digest}}") + echo "SENTRY_IMAGE_SHA=$SENTRY_IMAGE_SHA" >> $GITHUB_OUTPUT + SNUBA_IMAGE_SHA=$(docker buildx imagetools inspect $SNUBA_IMAGE --format "{{println .Manifest.Digest}}") + echo "SNUBA_IMAGE_SHA=$SNUBA_IMAGE_SHA" >> $GITHUB_OUTPUT + + - name: Restore DB Volumes Cache + id: restore_cache + uses: actions/cache/restore@v4 + with: + key: db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }}-${{ steps.cache_key.outputs.SNUBA_IMAGE_SHA }} + restore-keys: | + db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }} + db-volumes-v4- + path: | + /var/lib/docker/volumes/sentry-postgres/_data + /var/lib/docker/volumes/sentry-clickhouse/_data + /var/lib/docker/volumes/sentry-kafka/_data + + - name: Install self-hosted + env: + SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} + shell: bash + run: | + # This is for the cache restore on Kafka to work in older releases + docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data + # Add some customizations to test that path + cat <> sentry/enhance-image.sh + #!/bin/bash + touch /created-by-enhance-image + apt-get update + apt-get install -y gcc libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev + EOT + chmod 755 sentry/enhance-image.sh + echo "python-ldap" > sentry/requirements.txt + + ./install.sh --no-report-self-hosted-issues --skip-commit-check + + - name: Prepare Docker Volume Caching + shell: bash + run: | + # Set permissions for docker volumes so we can cache and restore + # We need these for the backup/restore test snapshotting too + sudo chmod o+x /var/lib/docker + sudo chmod -R o+rx /var/lib/docker/volumes + # Set tar ownership for it to be able to read + # From: https://github.com/actions/toolkit/issues/946#issuecomment-1726311681 + sudo chown root /usr/bin/tar && sudo chmod u+s /usr/bin/tar + sudo chown root /usr/bin/rsync && sudo chmod u+s /usr/bin/rsync + + - name: Save DB Volumes Cache + if: steps.restore_cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + key: ${{ steps.restore_cache.outputs.cache-primary-key }} + path: | + /var/lib/docker/volumes/sentry-postgres/_data + /var/lib/docker/volumes/sentry-clickhouse/_data + /var/lib/docker/volumes/sentry-kafka/_data + + - name: Integration Test + shell: bash + run: | + rsync -aW --no-compress --mkpath \ + /var/lib/docker/volumes/sentry-postgres \ + /var/lib/docker/volumes/sentry-clickhouse \ + /var/lib/docker/volumes/sentry-kafka \ + "$RUNNER_TEMP/volumes/" + docker compose up --wait + TEST_CUSTOMIZATIONS=enabled pytest -x --cov --junitxml=junit.xml _integration-test/ + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + if: inputs.CODECOV_TOKEN + with: + token: ${{ inputs.CODECOV_TOKEN }} + slug: getsentry/self-hosted + + - name: Upload test results to Codecov + if: inputs.CODECOV_TOKEN && !cancelled() + uses: codecov/test-results-action@v1 + with: + token: ${{ inputs.CODECOV_TOKEN }} From b948b10843db317cf6649cf5069aeae01b58f2c2 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 10 Jan 2025 22:08:13 +0000 Subject: [PATCH 128/287] fix: Fix the new e2e action to be portable (#3520) See errors on getsentry/snuba#6746 --- action.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/action.yaml b/action.yaml index 537be8c8260..6ca973e98c4 100644 --- a/action.yaml +++ b/action.yaml @@ -13,20 +13,17 @@ inputs: runs: using: "composite" steps: - - name: Go into self-hosted directory - shell: bash - run: cd ${{ github.action_path }} - - name: Configure to use the test image if: inputs.project_name && inputs.image_url shell: bash run: | image_var=$(echo ${{ inputs.project_name }}_IMAGE | tr '[:lower:]' '[:upper:]') - echo "${image_var}=${{ inputs.image_url }}" >> $GITHUB_ENV + echo "${image_var}=${{ inputs.image_url }}" >> ${{ github.action_path }}/.env - name: Setup dev environment shell: bash run: | + cd ${{ github.action_path }} pip install -r requirements-dev.txt echo "PY_COLORS=1" >> "$GITHUB_ENV" ### pytest-sentry configuration ### @@ -64,7 +61,7 @@ runs: # Set permissions for docker volumes so we can cache and restore sudo chmod o+x /var/lib/docker sudo chmod -R o+rwx /var/lib/docker/volumes - source .env + source ${{ github.action_path }}/.env SENTRY_IMAGE_SHA=$(docker buildx imagetools inspect $SENTRY_IMAGE --format "{{println .Manifest.Digest}}") echo "SENTRY_IMAGE_SHA=$SENTRY_IMAGE_SHA" >> $GITHUB_OUTPUT SNUBA_IMAGE_SHA=$(docker buildx imagetools inspect $SNUBA_IMAGE --format "{{println .Manifest.Digest}}") @@ -88,6 +85,7 @@ runs: SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} shell: bash run: | + cd ${{ github.action_path }} # This is for the cache restore on Kafka to work in older releases docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data # Add some customizations to test that path @@ -132,6 +130,7 @@ runs: /var/lib/docker/volumes/sentry-clickhouse \ /var/lib/docker/volumes/sentry-kafka \ "$RUNNER_TEMP/volumes/" + cd ${{ github.action_path }} docker compose up --wait TEST_CUSTOMIZATIONS=enabled pytest -x --cov --junitxml=junit.xml _integration-test/ @@ -139,6 +138,7 @@ runs: uses: codecov/codecov-action@v5 if: inputs.CODECOV_TOKEN with: + directory: ${{ github.action_path }} token: ${{ inputs.CODECOV_TOKEN }} slug: getsentry/self-hosted @@ -146,4 +146,5 @@ runs: if: inputs.CODECOV_TOKEN && !cancelled() uses: codecov/test-results-action@v1 with: + directory: ${{ github.action_path }} token: ${{ inputs.CODECOV_TOKEN }} From f97a5e2390455d7f5b368cb394730c1206394660 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 11 Jan 2025 21:59:13 +0000 Subject: [PATCH 129/287] ci: Faster and smarter backup/restore tests (#3516) From ``` ================== 11 passed, 4 warnings in 762.35s (0:12:42) ================== ``` to ``` ================== 11 passed, 4 warnings in 343.58s (0:05:43) ================== ``` --- _integration-test/conftest.py | 43 +-------------------------- _integration-test/test_backup.py | 50 ++++++++++++++++++++++++-------- _integration-test/test_run.py | 15 ++++++---- action.yaml | 2 +- install/bootstrap-snuba.sh | 6 +++- 5 files changed, 55 insertions(+), 61 deletions(-) diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py index b36097d6056..e80cef95e6a 100644 --- a/_integration-test/conftest.py +++ b/_integration-test/conftest.py @@ -1,58 +1,17 @@ import os +from os.path import join import subprocess -import time -import httpx import pytest SENTRY_CONFIG_PY = "sentry/sentry.conf.py" SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000/") TEST_USER = "test@example.com" TEST_PASS = "test123TEST" -TIMEOUT_SECONDS = 60 - - -def pytest_addoption(parser): - parser.addoption("--customizations", default="disabled") @pytest.fixture(scope="session", autouse=True) def configure_self_hosted_environment(request): - subprocess.run( - ["docker", "compose", "--ansi", "never", "up", "-d"], - check=True, - capture_output=True, - ) - for i in range(TIMEOUT_SECONDS): - try: - response = httpx.get(SENTRY_TEST_HOST, follow_redirects=True) - except httpx.RequestError: - time.sleep(1) - else: - if response.status_code == 200: - break - else: - raise AssertionError("timeout waiting for self-hosted to come up") - - if request.config.getoption("--customizations") == "enabled": - os.environ["TEST_CUSTOMIZATIONS"] = "enabled" - script_content = """\ -#!/bin/bash -touch /created-by-enhance-image -apt-get update -apt-get install -y gcc libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev -""" - - with open("sentry/enhance-image.sh", "w") as script_file: - script_file.write(script_content) - # Set executable permissions for the shell script - os.chmod("sentry/enhance-image.sh", 0o755) - - # Write content to the requirements.txt file - with open("sentry/requirements.txt", "w") as req_file: - req_file.write("python-ldap\n") - os.environ["MINIMIZE_DOWNTIME"] = "1" - subprocess.run(["./install.sh"], check=True, capture_output=True) # Create test user subprocess.run( [ diff --git a/_integration-test/test_backup.py b/_integration-test/test_backup.py index 41c099741a2..b73e0cfb163 100644 --- a/_integration-test/test_backup.py +++ b/_integration-test/test_backup.py @@ -1,4 +1,5 @@ import os +from os.path import join import subprocess @@ -20,7 +21,7 @@ def test_sentry_admin(setup_backup_restore_env_variables): def test_backup(setup_backup_restore_env_variables): - # Docker was giving me permissioning issues when trying to create this file and write to it even after giving read + write access + # Docker was giving me permission issues when trying to create this file and write to it even after giving read + write access # to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file file_path = os.path.join(os.getcwd(), "sentry", "backup.json") sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") @@ -42,21 +43,46 @@ def test_backup(setup_backup_restore_env_variables): def test_import(setup_backup_restore_env_variables): # Bring postgres down and recreate the docker volume + subprocess.run(["docker", "compose", "--ansi", "never", "down"], check=True) + # We reset all DB-related volumes here and not just Postgres although the backups + # are only for Postgres. The reason is to get a "clean slate" as we need the Kafka + # and Clickhouse volumes to be back to their initial state as well ( without any events) + # We cannot just rm and create them as they still need migrations. + for name in ("postgres", "clickhouse", "kafka"): + subprocess.run(["docker", "volume", "rm", f"sentry-{name}"], check=True) + subprocess.run( + [ + "rsync", + "-aW", + "--no-compress", + "--mkpath", + join(os.environ["RUNNER_TEMP"], "volumes", f"sentry-{name}", ""), + f"/var/lib/docker/volumes/sentry-{name}/", + ], + check=True, + capture_output=True, + ) + subprocess.run(["docker", "volume", "create", f"sentry-{name}"], check=True) + subprocess.run( - ["docker", "compose", "--ansi", "never", "stop", "postgres"], check=True - ) - subprocess.run( - ["docker", "compose", "--ansi", "never", "rm", "-f", "-v", "postgres"], - check=True, - ) - subprocess.run(["docker", "volume", "rm", "sentry-postgres"], check=True) - subprocess.run(["docker", "volume", "create", "--name=sentry-postgres"], check=True) - subprocess.run( - ["docker", "compose", "--ansi", "never", "run", "web", "upgrade", "--noinput"], + [ + "docker", + "run", + "--rm", + "-v", + "sentry-kafka:/data", + "busybox", + "chown", + "-R", + "1000:1000", + "/data", + ], check=True, + capture_output=True, ) + subprocess.run( - ["docker", "compose", "--ansi", "never", "up", "-d"], + ["docker", "compose", "--ansi", "never", "up", "--wait"], check=True, capture_output=True, ) diff --git a/_integration-test/test_run.py b/_integration-test/test_run.py index d1c8f4547c2..110de083855 100644 --- a/_integration-test/test_run.py +++ b/_integration-test/test_run.py @@ -326,7 +326,15 @@ def test_custom_certificate_authorities(): ) subprocess.run( - ["docker", "compose", "--ansi", "never", "up", "-d", "fixture-custom-ca-roots"], + [ + "docker", + "compose", + "--ansi", + "never", + "up", + "--wait", + "fixture-custom-ca-roots", + ], check=True, ) subprocess.run( @@ -448,7 +456,4 @@ def test_customizations(): ] for command in commands: result = subprocess.run(command, check=False) - if os.getenv("TEST_CUSTOMIZATIONS", "disabled") == "enabled": - assert result.returncode == 0 - else: - assert result.returncode != 0 + assert result.returncode == 0 diff --git a/action.yaml b/action.yaml index 6ca973e98c4..24baf35d6b4 100644 --- a/action.yaml +++ b/action.yaml @@ -132,7 +132,7 @@ runs: "$RUNNER_TEMP/volumes/" cd ${{ github.action_path }} docker compose up --wait - TEST_CUSTOMIZATIONS=enabled pytest -x --cov --junitxml=junit.xml _integration-test/ + pytest -x --cov --junitxml=junit.xml _integration-test/ - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 diff --git a/install/bootstrap-snuba.sh b/install/bootstrap-snuba.sh index 489c4da0e17..20666302ade 100644 --- a/install/bootstrap-snuba.sh +++ b/install/bootstrap-snuba.sh @@ -1,5 +1,9 @@ echo "${_group}Bootstrapping and migrating Snuba ..." -$dcr snuba-api bootstrap --force +if [[ -z "${SKIP_DB_MIGRATIONS:-}" ]]; then + $dcr snuba-api bootstrap --force +else + echo "Skipped DB migrations due to SKIP_DB_MIGRATIONS=$SKIP_DB_MIGRATIONS" +fi echo "${_endgroup}" From b439c67e17120c9fdd20aec5250a6357f78bd6c0 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 13 Jan 2025 03:00:52 +0700 Subject: [PATCH 130/287] docs: include regular env file on wrap-up (#3523) --- install/wrap-up.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/wrap-up.sh b/install/wrap-up.sh index dcc4e33c218..6f242284fe6 100644 --- a/install/wrap-up.sh +++ b/install/wrap-up.sh @@ -20,7 +20,7 @@ else echo "You're all done! Run the following command to get Sentry running:" echo "" if [[ "${_ENV}" =~ ".env.custom" ]]; then - echo " $dc_base --env-file ${_ENV} up --wait" + echo " $dc_base --env-file .env --env-file ${_ENV} up --wait" else echo " $dc_base up --wait" fi From d807ca4277a80d0ead633b0f7b8fb120027024f5 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Mon, 13 Jan 2025 20:35:31 +0000 Subject: [PATCH 131/287] ci: Less volatile cache keys (#3522) Instead of using direct image hashes, only use hashes from migrations folders for each respective image for cache key generation. Should increase cache hit rate significantly as we don't have migrations much. Also swaps the key order from `sentry-snuba` to `snuba-senry` assuming Snuba has less frequent migration additions. --- .github/workflows/test.yml | 14 +++++++------- action.yaml | 16 +++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4dff1aa5fee..ed1547909a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,19 +63,19 @@ jobs: sudo chmod o+x /var/lib/docker sudo chmod -R o+rwx /var/lib/docker/volumes source .env - SENTRY_IMAGE_SHA=$(docker buildx imagetools inspect $SENTRY_IMAGE --format "{{println .Manifest.Digest}}") - echo "SENTRY_IMAGE_SHA=$SENTRY_IMAGE_SHA" >> $GITHUB_OUTPUT - SNUBA_IMAGE_SHA=$(docker buildx imagetools inspect $SNUBA_IMAGE --format "{{println .Manifest.Digest}}") - echo "SNUBA_IMAGE_SHA=$SNUBA_IMAGE_SHA" >> $GITHUB_OUTPUT + SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c 'ls -Rv1rpq src/sentry/migrations/' | md5sum | cut -d ' ' -f 1) + echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT + SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c 'ls -Rv1rpq snuba/snuba_migrations/**/*.py' | md5sum | cut -d ' ' -f 1) + echo "SNUBA_MIGRATIONS_MD5=$SNUBA_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - name: Restore DB Volumes Cache id: restore_cache uses: actions/cache/restore@v4 with: - key: db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }}-${{ steps.cache_key.outputs.SNUBA_IMAGE_SHA }} + key: db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }} - db-volumes-v4- + db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + db-volumes-v5- path: | /var/lib/docker/volumes/sentry-postgres/_data /var/lib/docker/volumes/sentry-clickhouse/_data diff --git a/action.yaml b/action.yaml index 24baf35d6b4..fe2cc073ec4 100644 --- a/action.yaml +++ b/action.yaml @@ -62,19 +62,21 @@ runs: sudo chmod o+x /var/lib/docker sudo chmod -R o+rwx /var/lib/docker/volumes source ${{ github.action_path }}/.env - SENTRY_IMAGE_SHA=$(docker buildx imagetools inspect $SENTRY_IMAGE --format "{{println .Manifest.Digest}}") - echo "SENTRY_IMAGE_SHA=$SENTRY_IMAGE_SHA" >> $GITHUB_OUTPUT - SNUBA_IMAGE_SHA=$(docker buildx imagetools inspect $SNUBA_IMAGE --format "{{println .Manifest.Digest}}") - echo "SNUBA_IMAGE_SHA=$SNUBA_IMAGE_SHA" >> $GITHUB_OUTPUT + # See https://explainshell.com/explain?cmd=ls%20-Rv1rpq + # for that long `ls` command + SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c 'ls -Rv1rpq src/sentry/migrations/' | md5sum | cut -d ' ' -f 1) + echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT + SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c 'ls -Rv1rpq snuba/snuba_migrations/**/*.py' | md5sum | cut -d ' ' -f 1) + echo "SNUBA_MIGRATIONS_MD5=$SNUBA_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - name: Restore DB Volumes Cache id: restore_cache uses: actions/cache/restore@v4 with: - key: db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }}-${{ steps.cache_key.outputs.SNUBA_IMAGE_SHA }} + key: db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-v4-${{ steps.cache_key.outputs.SENTRY_IMAGE_SHA }} - db-volumes-v4- + db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + db-volumes-v5- path: | /var/lib/docker/volumes/sentry-postgres/_data /var/lib/docker/volumes/sentry-clickhouse/_data From f21b16d0ec6fb2d956dbd4d6b06fba0fdefbbc79 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 14 Jan 2025 22:09:11 +0000 Subject: [PATCH 132/287] ci: Use generic Docker volume cache action (#3524) See https://github.com/BYK/docker-volume-cache-action --- .github/workflows/test.yml | 47 +++++++++++++--------------------- action.yaml | 48 ++++++++++++----------------------- install/upgrade-clickhouse.sh | 2 +- 3 files changed, 35 insertions(+), 62 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed1547909a3..3e471c84baa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,12 +56,9 @@ jobs: sudo curl -L https://github.com/docker/compose/releases/download/v2.26.0/docker-compose-`uname -s`-`uname -m` -o "/usr/local/lib/docker/cli-plugins/docker-compose" sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" - - name: Prepare Docker Volume Caching + - name: Compute Docker Volume Cache Key id: cache_key run: | - # Set permissions for docker volumes so we can cache and restore - sudo chmod o+x /var/lib/docker - sudo chmod -R o+rwx /var/lib/docker/volumes source .env SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c 'ls -Rv1rpq src/sentry/migrations/' | md5sum | cut -d ' ' -f 1) echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT @@ -70,44 +67,36 @@ jobs: - name: Restore DB Volumes Cache id: restore_cache - uses: actions/cache/restore@v4 + uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} + key: db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} - db-volumes-v5- - path: | - /var/lib/docker/volumes/sentry-postgres/_data - /var/lib/docker/volumes/sentry-clickhouse/_data - /var/lib/docker/volumes/sentry-kafka/_data + db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + db-volumes-v6- + volumes: | + sentry-postgres + sentry-clickhouse + sentry-kafka - name: Install ${{ env.LATEST_TAG }} env: SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} run: | - # This is for the cache restore on Kafka to work in older releases - docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data + # This is to compensate for a bug in upgrade-clickhouse where + # if we have sentry-clickhouse volume without the rest, it fails + # We may get sentry-clickhouse from the cache step above + source install/create-docker-volumes.sh ./install.sh - - name: Prepare Docker Volume Caching - run: | - # Set permissions for docker volumes so we can cache and restore - # We need these for the backup/restore test snapshotting too - sudo chmod o+x /var/lib/docker - sudo chmod -R o+rx /var/lib/docker/volumes - # Set tar ownership for it to be able to read - # From: https://github.com/actions/toolkit/issues/946#issuecomment-1726311681 - sudo chown root /usr/bin/tar && sudo chmod u+s /usr/bin/tar - - name: Save DB Volumes Cache if: steps.restore_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: BYK/docker-volume-cache-action/save@be89365902126f508dcae387a32ec3712df6b1cd with: key: ${{ steps.restore_cache.outputs.cache-primary-key }} - path: | - /var/lib/docker/volumes/sentry-postgres/_data - /var/lib/docker/volumes/sentry-clickhouse/_data - /var/lib/docker/volumes/sentry-kafka/_data + volumes: | + sentry-postgres + sentry-clickhouse + sentry-kafka - name: Checkout current ref uses: actions/checkout@v4 diff --git a/action.yaml b/action.yaml index fe2cc073ec4..ec5897f0a27 100644 --- a/action.yaml +++ b/action.yaml @@ -18,7 +18,7 @@ runs: shell: bash run: | image_var=$(echo ${{ inputs.project_name }}_IMAGE | tr '[:lower:]' '[:upper:]') - echo "${image_var}=${{ inputs.image_url }}" >> ${{ github.action_path }}/.env + echo "${image_var}=${{ inputs.image_url }}" >> ${{ github.action_path }}.env - name: Setup dev environment shell: bash @@ -54,13 +54,10 @@ runs: sudo curl -L https://github.com/docker/compose/releases/download/${{ env.COMPOSE_VERSION }}/docker-compose-`uname -s`-`uname -m` -o "${{ env.COMPOSE_PATH }}/docker-compose" sudo chmod +x "${{ env.COMPOSE_PATH }}/docker-compose" - - name: Prepare Docker Volume Caching + - name: Compute Docker Volume Cache Key id: cache_key shell: bash run: | - # Set permissions for docker volumes so we can cache and restore - sudo chmod o+x /var/lib/docker - sudo chmod -R o+rwx /var/lib/docker/volumes source ${{ github.action_path }}/.env # See https://explainshell.com/explain?cmd=ls%20-Rv1rpq # for that long `ls` command @@ -71,16 +68,16 @@ runs: - name: Restore DB Volumes Cache id: restore_cache - uses: actions/cache/restore@v4 + uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} + key: db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-v5-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} - db-volumes-v5- - path: | - /var/lib/docker/volumes/sentry-postgres/_data - /var/lib/docker/volumes/sentry-clickhouse/_data - /var/lib/docker/volumes/sentry-kafka/_data + db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + db-volumes-v6- + volumes: | + sentry-postgres + sentry-clickhouse + sentry-kafka - name: Install self-hosted env: @@ -88,8 +85,6 @@ runs: shell: bash run: | cd ${{ github.action_path }} - # This is for the cache restore on Kafka to work in older releases - docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data # Add some customizations to test that path cat <> sentry/enhance-image.sh #!/bin/bash @@ -102,31 +97,20 @@ runs: ./install.sh --no-report-self-hosted-issues --skip-commit-check - - name: Prepare Docker Volume Caching - shell: bash - run: | - # Set permissions for docker volumes so we can cache and restore - # We need these for the backup/restore test snapshotting too - sudo chmod o+x /var/lib/docker - sudo chmod -R o+rx /var/lib/docker/volumes - # Set tar ownership for it to be able to read - # From: https://github.com/actions/toolkit/issues/946#issuecomment-1726311681 - sudo chown root /usr/bin/tar && sudo chmod u+s /usr/bin/tar - sudo chown root /usr/bin/rsync && sudo chmod u+s /usr/bin/rsync - - name: Save DB Volumes Cache if: steps.restore_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: BYK/docker-volume-cache-action/save@be89365902126f508dcae387a32ec3712df6b1cd with: key: ${{ steps.restore_cache.outputs.cache-primary-key }} - path: | - /var/lib/docker/volumes/sentry-postgres/_data - /var/lib/docker/volumes/sentry-clickhouse/_data - /var/lib/docker/volumes/sentry-kafka/_data + volumes: | + sentry-postgres + sentry-clickhouse + sentry-kafka - name: Integration Test shell: bash run: | + sudo chown root /usr/bin/rsync && sudo chmod u+s /usr/bin/rsync rsync -aW --no-compress --mkpath \ /var/lib/docker/volumes/sentry-postgres \ /var/lib/docker/volumes/sentry-clickhouse \ diff --git a/install/upgrade-clickhouse.sh b/install/upgrade-clickhouse.sh index 05e74bb00b9..bd69d7bf746 100644 --- a/install/upgrade-clickhouse.sh +++ b/install/upgrade-clickhouse.sh @@ -1,7 +1,7 @@ echo "${_group}Upgrading Clickhouse ..." # First check to see if user is upgrading by checking for existing clickhouse volume -if [[ -n "$(docker volume ls -q --filter name=sentry-clickhouse)" ]]; then +if docker compose ps -a | grep -q clickhouse; then # Start clickhouse if it is not already running $dc up --wait clickhouse From 11faae66acdc6a1dcbc72a09d8ee316f52daeadb Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 15 Jan 2025 18:05:59 +0000 Subject: [PATCH 133/287] release: 25.1.0 --- .env | 10 +++++----- CHANGELOG.md | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5688114306b..289ea70edcf 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.1.0 +SNUBA_IMAGE=getsentry/snuba:25.1.0 +RELAY_IMAGE=getsentry/relay:25.1.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.1.0 +VROOM_IMAGE=getsentry/vroom:25.1.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 56d9cfebee2..6da8315bbf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 25.1.0 + +### Various fixes & improvements + +- ci: Use generic Docker volume cache action (#3524) by @BYK +- ci: Less volatile cache keys (#3522) by @BYK +- docs: include regular env file on wrap-up (#3523) by @aldy505 +- ci: Faster and smarter backup/restore tests (#3516) by @BYK +- fix: Fix the new e2e action to be portable (#3520) by @BYK +- ci: Move e2e test action into the repo (#3519) by @BYK +- ci: Only test on compose 2.26 w/ customizations (#3506) by @BYK +- ci: Skip DB ops during install completely on cache hit (#3496) by @BYK +- chore: Remove everything zookeeper (#3499) by @hubertdeng123 +- ci: Cache postgres volume after first migration (#3488) by @BYK +- fix: Remove the extra space in the log file names (#3212) by @melnele +- ref(snuba): Combine bootstrap & migrate for faster bootstrap (#3491) by @BYK +- ref(geoip): Remove geoipupdate from compose (#3490) by @BYK +- build(deps): bump actions/create-github-app-token from 1.11.0 to 1.11.1 (#3492) by @dependabot + ## 24.12.1 ### Various fixes & improvements From c075cf570a2b41a489220dd2df68b74116dc3dfa Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 15 Jan 2025 18:25:34 +0000 Subject: [PATCH 134/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 289ea70edcf..5688114306b 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.1.0 -SNUBA_IMAGE=getsentry/snuba:25.1.0 -RELAY_IMAGE=getsentry/relay:25.1.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.1.0 -VROOM_IMAGE=getsentry/vroom:25.1.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 63b6c0afa7798e18c33203fa3290ae0254c77ddf Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 16 Jan 2025 17:59:38 +0000 Subject: [PATCH 135/287] test: Reorganize backup/restore tests for speed and reliability (#3537) We should do the backup/restore tests _after_ we do the basic tests. This is both more efficient as we avoid an extra up/down cycle and more meaningful as we will back up and restore an actually used system. A bit hard to measure directly as this also moves the initial `docker compose up -w` into the test suite but a random run without this patch took about 10m 49s to finish for the testing part whereas with the patch it came down to 9m 10s so **almost 2 minutes faster**! --- _integration-test/conftest.py | 5 ++++ .../{test_run.py => test_01_basics.py} | 0 .../{test_backup.py => test_02_backup.py} | 30 +++++-------------- action.yaml | 3 +- sentry-admin.sh | 3 +- 5 files changed, 16 insertions(+), 25 deletions(-) rename _integration-test/{test_run.py => test_01_basics.py} (100%) rename _integration-test/{test_backup.py => test_02_backup.py} (84%) diff --git a/_integration-test/conftest.py b/_integration-test/conftest.py index e80cef95e6a..e73a7f286c5 100644 --- a/_integration-test/conftest.py +++ b/_integration-test/conftest.py @@ -12,6 +12,11 @@ @pytest.fixture(scope="session", autouse=True) def configure_self_hosted_environment(request): + subprocess.run( + ["docker", "compose", "--ansi", "never", "up", "--wait"], + check=True, + capture_output=True, + ) # Create test user subprocess.run( [ diff --git a/_integration-test/test_run.py b/_integration-test/test_01_basics.py similarity index 100% rename from _integration-test/test_run.py rename to _integration-test/test_01_basics.py diff --git a/_integration-test/test_backup.py b/_integration-test/test_02_backup.py similarity index 84% rename from _integration-test/test_backup.py rename to _integration-test/test_02_backup.py index b73e0cfb163..0898f9a951c 100644 --- a/_integration-test/test_backup.py +++ b/_integration-test/test_02_backup.py @@ -20,7 +20,7 @@ def test_sentry_admin(setup_backup_restore_env_variables): assert "Usage: ./sentry-admin.sh permissions" in output -def test_backup(setup_backup_restore_env_variables): +def test_01_backup(setup_backup_restore_env_variables): # Docker was giving me permission issues when trying to create this file and write to it even after giving read + write access # to group and owner. Instead, try creating the empty file and then give everyone write access to the backup file file_path = os.path.join(os.getcwd(), "sentry", "backup.json") @@ -41,19 +41,22 @@ def test_backup(setup_backup_restore_env_variables): assert os.path.getsize(file_path) > 0 -def test_import(setup_backup_restore_env_variables): +def test_02_import(setup_backup_restore_env_variables): # Bring postgres down and recreate the docker volume subprocess.run(["docker", "compose", "--ansi", "never", "down"], check=True) # We reset all DB-related volumes here and not just Postgres although the backups # are only for Postgres. The reason is to get a "clean slate" as we need the Kafka - # and Clickhouse volumes to be back to their initial state as well ( without any events) - # We cannot just rm and create them as they still need migrations. + # and Clickhouse volumes to be back to their initial state as well (without any events) + # We cannot just rm and create them as they still need the migrations. for name in ("postgres", "clickhouse", "kafka"): subprocess.run(["docker", "volume", "rm", f"sentry-{name}"], check=True) + subprocess.run(["docker", "volume", "create", f"sentry-{name}"], check=True) subprocess.run( [ "rsync", "-aW", + "--super", + "--numeric-ids", "--no-compress", "--mkpath", join(os.environ["RUNNER_TEMP"], "volumes", f"sentry-{name}", ""), @@ -62,24 +65,6 @@ def test_import(setup_backup_restore_env_variables): check=True, capture_output=True, ) - subprocess.run(["docker", "volume", "create", f"sentry-{name}"], check=True) - - subprocess.run( - [ - "docker", - "run", - "--rm", - "-v", - "sentry-kafka:/data", - "busybox", - "chown", - "-R", - "1000:1000", - "/data", - ], - check=True, - capture_output=True, - ) subprocess.run( ["docker", "compose", "--ansi", "never", "up", "--wait"], @@ -97,3 +82,4 @@ def test_import(setup_backup_restore_env_variables): ], check=True, ) + # TODO: Check something actually restored here like the test user we have from earlier diff --git a/action.yaml b/action.yaml index ec5897f0a27..9f87b93d4f1 100644 --- a/action.yaml +++ b/action.yaml @@ -111,13 +111,12 @@ runs: shell: bash run: | sudo chown root /usr/bin/rsync && sudo chmod u+s /usr/bin/rsync - rsync -aW --no-compress --mkpath \ + rsync -aW --super --numeric-ids --no-compress --mkpath \ /var/lib/docker/volumes/sentry-postgres \ /var/lib/docker/volumes/sentry-clickhouse \ /var/lib/docker/volumes/sentry-kafka \ "$RUNNER_TEMP/volumes/" cd ${{ github.action_path }} - docker compose up --wait pytest -x --cov --junitxml=junit.xml _integration-test/ - name: Upload coverage to Codecov diff --git a/sentry-admin.sh b/sentry-admin.sh index 3db5f2ed044..ef0e75e98a5 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -22,7 +22,8 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { - $dcr --quiet-pull -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 + $dc up postgres --wait + $dcr --no-deps --quiet-pull -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 } # Function to modify lines starting with `Usage: sentry` to say `Usage: ./sentry-admin.sh` instead. From 3913a9f0c7cfcaf5396d257ba097b0688468a036 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 16 Jan 2025 18:10:14 +0000 Subject: [PATCH 136/287] ci: Even better cache keys and granular caching (#3534) Just starting up services for Snuba or Sentry migrations takes up to a minute sometimes and we do this even when there are no migrations, just because one of the Sentry or Snuba migrations change. This patch splits the caches up so only the necessary one runs, saving further time. It also uses the `LATEST_TAG` as the cache key for upgrade tests as the image versions or data will never change for a certain tag once it is release. --- .github/workflows/test.yml | 16 +++---- action.yaml | 62 +++++++++++++++++++++----- install/bootstrap-snuba.sh | 4 +- install/set-up-and-migrate-database.sh | 4 +- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e471c84baa..5c01abc6c4a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,22 +56,13 @@ jobs: sudo curl -L https://github.com/docker/compose/releases/download/v2.26.0/docker-compose-`uname -s`-`uname -m` -o "/usr/local/lib/docker/cli-plugins/docker-compose" sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" - - name: Compute Docker Volume Cache Key - id: cache_key - run: | - source .env - SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c 'ls -Rv1rpq src/sentry/migrations/' | md5sum | cut -d ' ' -f 1) - echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c 'ls -Rv1rpq snuba/snuba_migrations/**/*.py' | md5sum | cut -d ' ' -f 1) - echo "SNUBA_MIGRATIONS_MD5=$SNUBA_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - - name: Restore DB Volumes Cache id: restore_cache uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} + key: db-volumes-v6-${{ env.LATEST_TAG }} restore-keys: | - db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + db-volumes-v6-${{ env.LATEST_TAG }} db-volumes-v6- volumes: | sentry-postgres @@ -80,7 +71,10 @@ jobs: - name: Install ${{ env.LATEST_TAG }} env: + # Remove SKIP_DB_MIGRATIONS after releasing 25.1.1 or 25.2.0 SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} + SKIP_SENTRY_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} + SKIP_SNUBA_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} run: | # This is to compensate for a bug in upgrade-clickhouse where # if we have sentry-clickhouse volume without the rest, it fails diff --git a/action.yaml b/action.yaml index 9f87b93d4f1..924e4c856d3 100644 --- a/action.yaml +++ b/action.yaml @@ -54,34 +54,58 @@ runs: sudo curl -L https://github.com/docker/compose/releases/download/${{ env.COMPOSE_VERSION }}/docker-compose-`uname -s`-`uname -m` -o "${{ env.COMPOSE_PATH }}/docker-compose" sudo chmod +x "${{ env.COMPOSE_PATH }}/docker-compose" - - name: Compute Docker Volume Cache Key + - name: Compute Docker Volume Cache Keys id: cache_key shell: bash run: | source ${{ github.action_path }}/.env # See https://explainshell.com/explain?cmd=ls%20-Rv1rpq # for that long `ls` command - SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c 'ls -Rv1rpq src/sentry/migrations/' | md5sum | cut -d ' ' -f 1) + SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c '{ ls -Rv1rpq src/sentry/migrations/; sed -n "/KAFKA_TOPIC_TO_CLUSTER/,/}/p" src/sentry/conf/server.py; }' | md5sum | cut -d ' ' -f 1) echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c 'ls -Rv1rpq snuba/snuba_migrations/**/*.py' | md5sum | cut -d ' ' -f 1) + SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c '{ ls -Rv1rpq snuba/snuba_migrations/**/*.py; sed -n "/^class Topic(Enum):/,/\\n\\n/p" snuba/utils/streams/topics.py; }' | md5sum | cut -d ' ' -f 1) echo "SNUBA_MIGRATIONS_MD5=$SNUBA_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - - name: Restore DB Volumes Cache - id: restore_cache + - name: Restore Sentry Volume Cache + id: restore_cache_sentry uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} + key: db-volumes-sentry-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-v6-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} - db-volumes-v6- + db-volumes-sentry-v1- volumes: | sentry-postgres + + - name: Restore Snuba Volume Cache + id: restore_cache_snuba + uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd + with: + key: db-volumes-snuba-v1-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + restore-keys: | + db-volumes-snuba-v1- + volumes: | sentry-clickhouse + + - name: Restore Kafka Volume Cache + id: restore_cache_kafka + uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd + with: + key: db-volumes-kafka-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + restore-keys: | + db-volumes-kafka-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + db-volumes-kafka-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }}- + db-volumes-kafka-v1- + volumes: | sentry-kafka - name: Install self-hosted env: - SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} + # Note that cache keys for Sentry and Snuba have their respective Kafka configs built into them + # and the Kafka volume cache is comprises both keys. This way we can omit the Kafka cache hit + # in here to still avoid running Sentry or Snuba migrations if only one of their Kafka config has + # changed. Heats up your head a bit but if you think about it, it makes sense. + SKIP_SENTRY_MIGRATIONS: ${{ steps.restore_cache_sentry.outputs.cache-hit == 'true' && '1' || '' }} + SKIP_SNUBA_MIGRATIONS: ${{ steps.restore_cache_snuba.outputs.cache-hit == 'true' && '1' || '' }} shell: bash run: | cd ${{ github.action_path }} @@ -97,14 +121,28 @@ runs: ./install.sh --no-report-self-hosted-issues --skip-commit-check - - name: Save DB Volumes Cache - if: steps.restore_cache.outputs.cache-hit != 'true' + - name: Save Sentry Volume Cache + if: steps.restore_cache_sentry.outputs.cache-hit != 'true' uses: BYK/docker-volume-cache-action/save@be89365902126f508dcae387a32ec3712df6b1cd with: - key: ${{ steps.restore_cache.outputs.cache-primary-key }} + key: ${{ steps.restore_cache_sentry.outputs.cache-primary-key }} volumes: | sentry-postgres + + - name: Save Snuba Volume Cache + if: steps.restore_cache_snuba.outputs.cache-hit != 'true' + uses: BYK/docker-volume-cache-action/save@be89365902126f508dcae387a32ec3712df6b1cd + with: + key: ${{ steps.restore_cache_snuba.outputs.cache-primary-key }} + volumes: | sentry-clickhouse + + - name: Save Kafka Volume Cache + if: steps.restore_cache_kafka.outputs.cache-hit != 'true' + uses: BYK/docker-volume-cache-action/save@be89365902126f508dcae387a32ec3712df6b1cd + with: + key: ${{ steps.restore_cache_kafka.outputs.cache-primary-key }} + volumes: | sentry-kafka - name: Integration Test diff --git a/install/bootstrap-snuba.sh b/install/bootstrap-snuba.sh index 20666302ade..496becd6c57 100644 --- a/install/bootstrap-snuba.sh +++ b/install/bootstrap-snuba.sh @@ -1,9 +1,9 @@ echo "${_group}Bootstrapping and migrating Snuba ..." -if [[ -z "${SKIP_DB_MIGRATIONS:-}" ]]; then +if [[ -z "${SKIP_SNUBA_MIGRATIONS:-}" ]]; then $dcr snuba-api bootstrap --force else - echo "Skipped DB migrations due to SKIP_DB_MIGRATIONS=$SKIP_DB_MIGRATIONS" + echo "Skipped DB migrations due to SKIP_SNUBA_MIGRATIONS=$SKIP_SNUBA_MIGRATIONS" fi echo "${_endgroup}" diff --git a/install/set-up-and-migrate-database.sh b/install/set-up-and-migrate-database.sh index a1cc8323e89..2d4e13208e3 100644 --- a/install/set-up-and-migrate-database.sh +++ b/install/set-up-and-migrate-database.sh @@ -1,6 +1,6 @@ echo "${_group}Setting up / migrating database ..." -if [[ -z "${SKIP_DB_MIGRATIONS:-}" ]]; then +if [[ -z "${SKIP_SENTRY_MIGRATIONS:-}" ]]; then # Fixes https://github.com/getsentry/self-hosted/issues/2758, where a migration fails due to indexing issue $dc up --wait postgres @@ -31,6 +31,6 @@ with connection.cursor() as cursor: $dcr web upgrade --create-kafka-topics fi else - echo "Skipped DB migrations due to SKIP_DB_MIGRATIONS=$SKIP_DB_MIGRATIONS" + echo "Skipped DB migrations due to SKIP_SENTRY_MIGRATIONS=$SKIP_SENTRY_MIGRATIONS" fi echo "${_endgroup}" From 559e7324686b8b0a7fa6f4783013cbb7a81d7080 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 16 Jan 2025 21:45:07 +0000 Subject: [PATCH 137/287] breaking: Upgrade min Compose version to 2.23.2 (#3535) In this version, there's a new `--pull` argument for `docker compose run` which we will start leveraging, especially with `sentry-admin` command. Should come with a slight speed boost. --- .github/workflows/test.yml | 16 +++++++--------- _unit-test/js-sdk-assets-test.sh | 7 ++++--- action.yaml | 17 ++--------------- get-compose-action/action.yaml | 19 +++++++++++++++++++ install/_min-requirements.sh | 2 +- install/dc-detect-version.sh | 2 +- sentry-admin.sh | 2 +- 7 files changed, 35 insertions(+), 30 deletions(-) create mode 100644 get-compose-action/action.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c01abc6c4a..af1e624dc2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Get Compose + uses: ./get-compose-action + - name: Unit Tests run: ./unit-test.sh @@ -42,20 +45,15 @@ jobs: LATEST_TAG=$(curl -s https://api.github.com/repos/getsentry/self-hosted/releases/latest | jq -r '.tag_name') echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + - name: Get Compose + # TODO: Replace this with `@master` after landing + uses: getsentry/self-hosted/get-compose-action@byk/ref/upgrade-compose + - name: Checkout latest release uses: actions/checkout@v4 with: ref: ${{ env.LATEST_TAG }} - - name: Get Compose - run: | - # Docker Compose v1 is installed here, remove it - sudo rm -f "/usr/local/bin/docker-compose" - sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" - sudo mkdir -p "/usr/local/lib/docker/cli-plugins" - sudo curl -L https://github.com/docker/compose/releases/download/v2.26.0/docker-compose-`uname -s`-`uname -m` -o "/usr/local/lib/docker/cli-plugins/docker-compose" - sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" - - name: Restore DB Volumes Cache id: restore_cache uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd diff --git a/_unit-test/js-sdk-assets-test.sh b/_unit-test/js-sdk-assets-test.sh index 7177f559ba9..bd898acf6a7 100755 --- a/_unit-test/js-sdk-assets-test.sh +++ b/_unit-test/js-sdk-assets-test.sh @@ -3,14 +3,15 @@ source _unit-test/_test_setup.sh source install/dc-detect-version.sh $dcb --force-rm web +$dc pull nginx export SETUP_JS_SDK_ASSETS=1 source install/setup-js-sdk-assets.sh -sdk_files=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx ls -lah /var/www/js-sdk/) -sdk_tree=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx tree /var/www/js-sdk/ | tail -n 1) -non_empty_file_count=$(docker compose run --no-deps --rm -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk/ -type f -size +1k | wc -l) +sdk_files=$($dcr --no-deps -v "sentry-nginx-www:/var/www" nginx ls -lah /var/www/js-sdk/) +sdk_tree=$($dcr --no-deps -v "sentry-nginx-www:/var/www" nginx tree /var/www/js-sdk/ | tail -n 1) +non_empty_file_count=$($dcr --no-deps -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk/ -type f -size +1k | wc -l) # `sdk_files` should contains 5 lines, '4.*', '5.*', '6.*', `7.*` and `8.*` echo $sdk_files diff --git a/action.yaml b/action.yaml index 924e4c856d3..846bca0a875 100644 --- a/action.yaml +++ b/action.yaml @@ -39,26 +39,13 @@ runs: fi - name: Get Compose - env: - COMPOSE_PATH: /usr/local/lib/docker/cli-plugins - COMPOSE_VERSION: "v2.26.0" - shell: bash - run: | - # Always remove `docker compose` support as that's the newer version - # and comes installed by default nowadays. - sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" - # Docker Compose v1 is installed here, remove it - sudo rm -f "/usr/local/bin/docker-compose" - sudo rm -f "${{ env.COMPOSE_PATH }}/docker-compose" - sudo mkdir -p "${{ env.COMPOSE_PATH }}" - sudo curl -L https://github.com/docker/compose/releases/download/${{ env.COMPOSE_VERSION }}/docker-compose-`uname -s`-`uname -m` -o "${{ env.COMPOSE_PATH }}/docker-compose" - sudo chmod +x "${{ env.COMPOSE_PATH }}/docker-compose" + uses: ./get-compose-action - name: Compute Docker Volume Cache Keys id: cache_key shell: bash run: | - source ${{ github.action_path }}/.env + source ${{ github.action_path }}.env # See https://explainshell.com/explain?cmd=ls%20-Rv1rpq # for that long `ls` command SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c '{ ls -Rv1rpq src/sentry/migrations/; sed -n "/KAFKA_TOPIC_TO_CLUSTER/,/}/p" src/sentry/conf/server.py; }' | md5sum | cut -d ' ' -f 1) diff --git a/get-compose-action/action.yaml b/get-compose-action/action.yaml new file mode 100644 index 00000000000..17d5993e70d --- /dev/null +++ b/get-compose-action/action.yaml @@ -0,0 +1,19 @@ +name: "Get Docker Compose" +inputs: + version: + required: false + default: 2.32.3 + description: "Docker Compose version" + +runs: + using: "composite" + steps: + - name: Get Compose + shell: bash + run: | + # Docker Compose v1 is installed here, remove it + sudo rm -f "/usr/local/bin/docker-compose" + sudo rm -f "/usr/local/lib/docker/cli-plugins/docker-compose" + sudo mkdir -p "/usr/local/lib/docker/cli-plugins" + sudo curl -L https://github.com/docker/compose/releases/download/v${{ inputs.version }}/docker-compose-`uname -s`-`uname -m` -o "/usr/local/lib/docker/cli-plugins/docker-compose" + sudo chmod +x "/usr/local/lib/docker/cli-plugins/docker-compose" diff --git a/install/_min-requirements.sh b/install/_min-requirements.sh index 2180c6a9a97..c518508e2f3 100644 --- a/install/_min-requirements.sh +++ b/install/_min-requirements.sh @@ -1,6 +1,6 @@ # Don't forget to update the README and other docs when you change these! MIN_DOCKER_VERSION='19.03.6' -MIN_COMPOSE_VERSION='2.19.0' +MIN_COMPOSE_VERSION='2.32.2' # 16 GB minimum host RAM, but there'll be some overhead outside of what # can be allotted to docker diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index 64e814e1ecd..ccee1b5eebe 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -16,7 +16,7 @@ else dc="$dc_base --ansi never" fi proxy_args="--build-arg http_proxy=${http_proxy:-} --build-arg https_proxy=${https_proxy:-} --build-arg no_proxy=${no_proxy:-}" -dcr="$dc run --rm" +dcr="$dc run --pull=never --rm" dcb="$dc build $proxy_args" dbuild="docker build $proxy_args" diff --git a/sentry-admin.sh b/sentry-admin.sh index ef0e75e98a5..e910cb1dc95 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -23,7 +23,7 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { $dc up postgres --wait - $dcr --no-deps --quiet-pull -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 + $dcr --no-deps -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 } # Function to modify lines starting with `Usage: sentry` to say `Usage: ./sentry-admin.sh` instead. From 52a1901f75409436ef14d48885d3499b3c3ebec2 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 16 Jan 2025 21:58:18 +0000 Subject: [PATCH 138/287] ci: Move self-contained action reference to master branch (#3538) Required follow up to #3535 --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index af1e624dc2f..8dba1227a40 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,8 +46,7 @@ jobs: echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV - name: Get Compose - # TODO: Replace this with `@master` after landing - uses: getsentry/self-hosted/get-compose-action@byk/ref/upgrade-compose + uses: getsentry/self-hosted/get-compose-action@master - name: Checkout latest release uses: actions/checkout@v4 From 7a897618df0f4c3b4d113684c40b74cb762a5a42 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 16 Jan 2025 23:16:18 -0800 Subject: [PATCH 139/287] fix: Caching of sentry migrations should cover additional folders (#3542) We need to care about more than just src/sentry/migrations. We will need to account for files in src/sentry/**/migrations/* Taken from https://github.com/getsentry/sentry/blob/afd74698180066223dee53991b7db26ca80ea3e5/.github/file-filters.yml#L90 --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 846bca0a875..7601c9057db 100644 --- a/action.yaml +++ b/action.yaml @@ -48,7 +48,7 @@ runs: source ${{ github.action_path }}.env # See https://explainshell.com/explain?cmd=ls%20-Rv1rpq # for that long `ls` command - SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c '{ ls -Rv1rpq src/sentry/migrations/; sed -n "/KAFKA_TOPIC_TO_CLUSTER/,/}/p" src/sentry/conf/server.py; }' | md5sum | cut -d ' ' -f 1) + SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c '{ ls -Rv1rpq src/sentry/**/migrations/*; sed -n "/KAFKA_TOPIC_TO_CLUSTER/,/}/p" src/sentry/conf/server.py; }' | md5sum | cut -d ' ' -f 1) echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c '{ ls -Rv1rpq snuba/snuba_migrations/**/*.py; sed -n "/^class Topic(Enum):/,/\\n\\n/p" snuba/utils/streams/topics.py; }' | md5sum | cut -d ' ' -f 1) echo "SNUBA_MIGRATIONS_MD5=$SNUBA_MIGRATIONS_MD5" >> $GITHUB_OUTPUT From 0ce2cd7e326e1e31df90469dbef734cdf5b4aacc Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 16 Jan 2025 23:31:22 -0800 Subject: [PATCH 140/287] fix: Use correct path for get compose action (#3539) --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 7601c9057db..c177615c725 100644 --- a/action.yaml +++ b/action.yaml @@ -39,7 +39,7 @@ runs: fi - name: Get Compose - uses: ./get-compose-action + uses: getsentry/self-hosted/get-compose-action@master - name: Compute Docker Volume Cache Keys id: cache_key From 9ceb00f739d478c83f19059dee7cc0b7121b11b8 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Fri, 17 Jan 2025 03:51:31 -0800 Subject: [PATCH 141/287] chore: Remove upgrade test (#3541) As discussed, removing the upgrade test since it doesn't provide that much utility as we are already testing upgrades from restoring docker volumes from cache --- .github/workflows/test.yml | 71 -------------------------------------- 1 file changed, 71 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8dba1227a40..672a2802e78 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,77 +33,6 @@ jobs: - name: Unit Tests run: ./unit-test.sh - upgrade-test: - if: github.repository_owner == 'getsentry' - runs-on: ubuntu-22.04 - name: "Sentry upgrade test" - env: - REPORT_SELF_HOSTED_ISSUES: 0 - steps: - - name: Get latest self-hosted release version - run: | - LATEST_TAG=$(curl -s https://api.github.com/repos/getsentry/self-hosted/releases/latest | jq -r '.tag_name') - echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV - - - name: Get Compose - uses: getsentry/self-hosted/get-compose-action@master - - - name: Checkout latest release - uses: actions/checkout@v4 - with: - ref: ${{ env.LATEST_TAG }} - - - name: Restore DB Volumes Cache - id: restore_cache - uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd - with: - key: db-volumes-v6-${{ env.LATEST_TAG }} - restore-keys: | - db-volumes-v6-${{ env.LATEST_TAG }} - db-volumes-v6- - volumes: | - sentry-postgres - sentry-clickhouse - sentry-kafka - - - name: Install ${{ env.LATEST_TAG }} - env: - # Remove SKIP_DB_MIGRATIONS after releasing 25.1.1 or 25.2.0 - SKIP_DB_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} - SKIP_SENTRY_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} - SKIP_SNUBA_MIGRATIONS: ${{ steps.restore_cache.outputs.cache-hit == 'true' && '1' || '' }} - run: | - # This is to compensate for a bug in upgrade-clickhouse where - # if we have sentry-clickhouse volume without the rest, it fails - # We may get sentry-clickhouse from the cache step above - source install/create-docker-volumes.sh - ./install.sh - - - name: Save DB Volumes Cache - if: steps.restore_cache.outputs.cache-hit != 'true' - uses: BYK/docker-volume-cache-action/save@be89365902126f508dcae387a32ec3712df6b1cd - with: - key: ${{ steps.restore_cache.outputs.cache-primary-key }} - volumes: | - sentry-postgres - sentry-clickhouse - sentry-kafka - - - name: Checkout current ref - uses: actions/checkout@v4 - - - name: Install current ref - run: | - # This is for the cache restore on Kafka to work in older releases - docker run --rm -v "sentry-kafka:/data" busybox chown -R 1000:1000 /data - ./install.sh - - - name: Inspect failure - if: failure() - run: | - docker compose ps - docker compose logs - integration-test: if: github.repository_owner == 'getsentry' runs-on: ubuntu-22.04 From 3984a876112f9c1c10a94479796fb979741b8310 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 17 Jan 2025 17:40:40 +0000 Subject: [PATCH 142/287] fix: github.action_path may not have trailing slash (#3547) --- action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yaml b/action.yaml index c177615c725..5370eda824f 100644 --- a/action.yaml +++ b/action.yaml @@ -18,7 +18,7 @@ runs: shell: bash run: | image_var=$(echo ${{ inputs.project_name }}_IMAGE | tr '[:lower:]' '[:upper:]') - echo "${image_var}=${{ inputs.image_url }}" >> ${{ github.action_path }}.env + echo "${image_var}=${{ inputs.image_url }}" >> ${{ github.action_path }}/.env - name: Setup dev environment shell: bash @@ -45,7 +45,7 @@ runs: id: cache_key shell: bash run: | - source ${{ github.action_path }}.env + source ${{ github.action_path }}/.env # See https://explainshell.com/explain?cmd=ls%20-Rv1rpq # for that long `ls` command SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c '{ ls -Rv1rpq src/sentry/**/migrations/*; sed -n "/KAFKA_TOPIC_TO_CLUSTER/,/}/p" src/sentry/conf/server.py; }' | md5sum | cut -d ' ' -f 1) From e1870f8ecbd9f2f2d707f45a702867148b7fd819 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 17 Jan 2025 20:29:23 +0000 Subject: [PATCH 143/287] ci: Remove obsolete `dcr up -w` from import test (#3544) I _think_ we can get away with this but let's see what the CI thinks. If it passes, it should save us another minuter or two. --- _integration-test/test_02_backup.py | 5 ----- sentry-admin.sh | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/_integration-test/test_02_backup.py b/_integration-test/test_02_backup.py index 0898f9a951c..ad42824524e 100644 --- a/_integration-test/test_02_backup.py +++ b/_integration-test/test_02_backup.py @@ -66,11 +66,6 @@ def test_02_import(setup_backup_restore_env_variables): capture_output=True, ) - subprocess.run( - ["docker", "compose", "--ansi", "never", "up", "--wait"], - check=True, - capture_output=True, - ) sentry_admin_sh = os.path.join(os.getcwd(), "sentry-admin.sh") subprocess.run( [ diff --git a/sentry-admin.sh b/sentry-admin.sh index e910cb1dc95..386b3d57011 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -23,6 +23,7 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { $dc up postgres --wait + $dc up redis --wait $dcr --no-deps -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 } From d637ed19fefb77cc7d6dab017b9da3ccceb257ca Mon Sep 17 00:00:00 2001 From: Chad Whitacre Date: Wed, 22 Jan 2025 15:15:53 -0500 Subject: [PATCH 144/287] Hand off open-source to dev-infra (#3549) --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f83c1c8e04b..31589ecff7a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ updates: interval: daily open-pull-requests-limit: 0 # only security updates reviewers: - - "@getsentry/open-source" + - "@getsentry/dev-infra" - "@getsentry/security" - package-ecosystem: "github-actions" @@ -15,5 +15,5 @@ updates: # Check for updates to GitHub Actions every week interval: "weekly" reviewers: - - "@getsentry/open-source" + - "@getsentry/dev-infra" - "@getsentry/security" From 95761f7d6259171b8d1b1bbbb4b595c3098159b3 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 24 Jan 2025 22:14:51 +0000 Subject: [PATCH 145/287] ref: Simpler and more accurate cache keys (#3553) --- action.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/action.yaml b/action.yaml index 5370eda824f..3bdf597bf3e 100644 --- a/action.yaml +++ b/action.yaml @@ -48,16 +48,16 @@ runs: source ${{ github.action_path }}/.env # See https://explainshell.com/explain?cmd=ls%20-Rv1rpq # for that long `ls` command - SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c '{ ls -Rv1rpq src/sentry/**/migrations/*; sed -n "/KAFKA_TOPIC_TO_CLUSTER/,/}/p" src/sentry/conf/server.py; }' | md5sum | cut -d ' ' -f 1) + SENTRY_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SENTRY_IMAGE -c '{ cat migrations_lockfile.txt; grep -Poz "(?s)(?<=class Topic\\(Enum\\):\\n).+?(?=\\n\\n\\n)" src/sentry/conf/types/kafka_definition.py; }' | md5sum | cut -d ' ' -f 1) echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c '{ ls -Rv1rpq snuba/snuba_migrations/**/*.py; sed -n "/^class Topic(Enum):/,/\\n\\n/p" snuba/utils/streams/topics.py; }' | md5sum | cut -d ' ' -f 1) + SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c '{ ls -Rv1rpq snuba/snuba_migrations/**/*.py; grep -Poz "(?s)(?<=class Topic\\(Enum\\):\\n).+?(?=\\n\\n\\n)" snuba/utils/streams/topics.py; }' | md5sum | cut -d ' ' -f 1) echo "SNUBA_MIGRATIONS_MD5=$SNUBA_MIGRATIONS_MD5" >> $GITHUB_OUTPUT - name: Restore Sentry Volume Cache id: restore_cache_sentry uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-sentry-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} + key: db-volumes-sentry-v2-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} restore-keys: | db-volumes-sentry-v1- volumes: | @@ -67,7 +67,7 @@ runs: id: restore_cache_snuba uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-snuba-v1-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + key: db-volumes-snuba-v2-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} restore-keys: | db-volumes-snuba-v1- volumes: | From 9eea781e1c189ebc2f2f757efa98b82f80fb7496 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 25 Jan 2025 16:53:50 +0000 Subject: [PATCH 146/287] feat: Require both inputs to be set on action (#3554) Came as a feature request from @untitaker and I think it makes a lot of sense --------- Co-authored-by: Hubert Deng --- action.yaml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/action.yaml b/action.yaml index 3bdf597bf3e..ebc22f4831c 100644 --- a/action.yaml +++ b/action.yaml @@ -13,12 +13,22 @@ inputs: runs: using: "composite" steps: - - name: Configure to use the test image - if: inputs.project_name && inputs.image_url + - name: Validate inputs and configure test image shell: bash + env: + PROJECT_NAME: ${{ inputs.project_name }} + IMAGE_URL: ${{ inputs.image_url }} run: | - image_var=$(echo ${{ inputs.project_name }}_IMAGE | tr '[:lower:]' '[:upper:]') - echo "${image_var}=${{ inputs.image_url }}" >> ${{ github.action_path }}/.env + if [[ -n "$PROJECT_NAME" && -n "$IMAGE_URL" ]]; then + image_var=$(echo "${PROJECT_NAME}_IMAGE" | tr '[:lower:]' '[:upper:]') + echo "${image_var}=$IMAGE_URL" >> ${{ github.action_path }}/.env + elif [[ -z "$PROJECT_NAME" && -z "$IMAGE_URL" ]]; then + echo "No project name and image URL set. Skipping image configuration." + else + echo "You must set both project_name and image_url or unset both." + echo "project_name: $PROJECT_NAME, image_url: $IMAGE_URL" + exit 1 + fi - name: Setup dev environment shell: bash From cfb0491f6341c8c8b66ac9b14056ae481f4e253c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:11:27 +0000 Subject: [PATCH 147/287] build(deps): bump actions/create-github-app-token from 1.11.1 to 1.11.2 (#3561) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.1 to 1.11.2. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/c1a285145b9d317df6ced56c09f525b5c2b6f755...136412a57a7081aa63c935a2cc2918f76c34f514) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38e929334f6..1b3f442263f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1 + uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From ee3cbf0f9101cca61033916d691f18d7a9ebb94e Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 4 Feb 2025 19:41:12 +0700 Subject: [PATCH 148/287] feat: merge `.env` and `.env.custom` file during installation (#3564) Closes https://github.com/getsentry/self-hosted/issues/3558 --- _unit-test/merge-env-file-test.sh | 25 +++++++++++++++++++++++++ install/_lib.sh | 10 +++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100755 _unit-test/merge-env-file-test.sh diff --git a/_unit-test/merge-env-file-test.sh b/_unit-test/merge-env-file-test.sh new file mode 100755 index 00000000000..156110d0060 --- /dev/null +++ b/_unit-test/merge-env-file-test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# This is a test file for a part of `_lib.sh`, where we read `.env.custom` file if there is one. +# We only want to give very minimal value to the `.env.custom` file, and expect that it would +# be merged with the original `.env` file, with the `.env.custom` file taking precedence. +cat <".env.custom" +SENTRY_EVENT_RETENTION_DAYS=10 +EOF + +# The `_test_setup.sh` script sources `install/_lib.sh`, so.. finger crossed this should works. +source _unit-test/_test_setup.sh + +rm -f .env.custom + +echo "Expecting SENTRY_EVENT_RETENTION_DAYS to be 10, got ${SENTRY_EVENT_RETENTION_DAYS}" +test "$SENTRY_EVENT_RETENTION_DAYS" == "10" +echo "Pass" +echo "Expecting SENTRY_BIND to be 9000, got ${SENTRY_BIND}" +test "$SENTRY_BIND" == "9000" +echo "Pass" +echo "Expecting COMPOSE_PROJECT_NAME to be sentry-self-hosted, got ${COMPOSE_PROJECT_NAME}" +test "$COMPOSE_PROJECT_NAME" == "sentry-self-hosted" +echo "Pass" + +report_success diff --git a/install/_lib.sh b/install/_lib.sh index 3a76d8624f0..73ef0237df9 100644 --- a/install/_lib.sh +++ b/install/_lib.sh @@ -16,8 +16,16 @@ else _ENV=.env fi +# Reading .env.custom has to come first. The value won't be overriden, instead +# it would persist because of `export -p> >"$t"` later, which exports current +# environment variables to a temporary file with a `declare -x KEY=value` format. +# The new values on `.env` would be set only if they are not already set. +if [[ "$_ENV" == ".env.custom" ]]; then + q=$(mktemp) && export -p >"$q" && set -a && . ".env.custom" && set +a && . "$q" && rm "$q" && unset q +fi + # Read .env for default values with a tip o' the hat to https://stackoverflow.com/a/59831605/90297 -t=$(mktemp) && export -p >"$t" && set -a && . $_ENV && set +a && . "$t" && rm "$t" && unset t +t=$(mktemp) && export -p >"$t" && set -a && . ".env" && set +a && . "$t" && rm "$t" && unset t if [ "${GITHUB_ACTIONS:-}" = "true" ]; then _group="::group::" From 3de8662160e8706a614638f50180d960331170d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:09:45 -0800 Subject: [PATCH 149/287] build(deps): bump actions/create-github-app-token from 1.11.2 to 1.11.3 (#3569) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.2 to 1.11.3. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/136412a57a7081aa63c935a2cc2918f76c34f514...67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b3f442263f..978be3c9a62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2 + uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From bc716095e409f7cb096b8bd55ed899b8e3727d49 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sat, 15 Feb 2025 18:05:45 +0000 Subject: [PATCH 150/287] release: 25.2.0 --- .env | 10 +++++----- CHANGELOG.md | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5688114306b..d44d5d5ab2f 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.2.0 +SNUBA_IMAGE=getsentry/snuba:25.2.0 +RELAY_IMAGE=getsentry/relay:25.2.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.2.0 +VROOM_IMAGE=getsentry/vroom:25.2.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6da8315bbf3..29741bb05c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 25.2.0 + +### Various fixes & improvements + +- build(deps): bump actions/create-github-app-token from 1.11.2 to 1.11.3 (#3569) by @dependabot +- feat: merge `.env` and `.env.custom` file during installation (#3564) by @aldy505 +- build(deps): bump actions/create-github-app-token from 1.11.1 to 1.11.2 (#3561) by @dependabot +- feat: Require both inputs to be set on action (#3554) by @BYK +- ref: Simpler and more accurate cache keys (#3553) by @BYK +- Hand off open-source to dev-infra (#3549) by @chadwhitacre +- ci: Remove obsolete `dcr up -w` from import test (#3544) by @BYK +- fix: github.action_path may not have trailing slash (#3547) by @BYK +- chore: Remove upgrade test (#3541) by @hubertdeng123 +- fix: Use correct path for get compose action (#3539) by @hubertdeng123 +- fix: Caching of sentry migrations should cover additional folders (#3542) by @hubertdeng123 +- ci: Move self-contained action reference to master branch (#3538) by @BYK +- breaking: Upgrade min Compose version to 2.23.2 (#3535) by @BYK +- ci: Even better cache keys and granular caching (#3534) by @BYK +- test: Reorganize backup/restore tests for speed and reliability (#3537) by @BYK + ## 25.1.0 ### Various fixes & improvements From 2f2631840a3cbe9dd8fdbac45abe44f5021b3b34 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 18 Feb 2025 19:52:58 +0000 Subject: [PATCH 151/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index d44d5d5ab2f..5688114306b 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.2.0 -SNUBA_IMAGE=getsentry/snuba:25.2.0 -RELAY_IMAGE=getsentry/relay:25.2.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.2.0 -VROOM_IMAGE=getsentry/vroom:25.2.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 3e1ebfb4fcb087d40f87339fd1a502780b66782a Mon Sep 17 00:00:00 2001 From: leeoocca <36135198+leeoocca@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:32:25 +0100 Subject: [PATCH 152/287] refactor: move system.url-prefix under systems settings section (#3588) --- sentry/config.example.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry/config.example.yml b/sentry/config.example.yml index d5a6dc322ce..0764183a199 100644 --- a/sentry/config.example.yml +++ b/sentry/config.example.yml @@ -45,6 +45,10 @@ mail.host: 'smtp' # System Settings # ################### +# The URL prefix in which Sentry is accessible +# system.url-prefix: https://example.sentry.com +system.internal-url-prefix: 'http://web:9000/' + # If this file ever becomes compromised, it's important to generate a new key. # Changing this value will result in all current sessions being invalidated. # A new key can be generated with `$ sentry config generate-secret-key` @@ -79,9 +83,6 @@ releasefile.cache-path: '/data/releasefile-cache' # secret_key: 'XXXXXXX' # bucket_name: 's3-bucket-name' -# The URL prefix in which Sentry is accessible -# system.url-prefix: https://example.sentry.com -system.internal-url-prefix: 'http://web:9000/' symbolicator.enabled: true symbolicator.options: url: "http://symbolicator:3021/" From e883f21b33a54d83342a5bc73eaef63fab27079d Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Mon, 3 Mar 2025 00:34:37 +0330 Subject: [PATCH 153/287] Bump docker-compose 2.33.1 (#3597) Merging to get things green --- get-compose-action/action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/get-compose-action/action.yaml b/get-compose-action/action.yaml index 17d5993e70d..7fac08d3489 100644 --- a/get-compose-action/action.yaml +++ b/get-compose-action/action.yaml @@ -2,7 +2,7 @@ name: "Get Docker Compose" inputs: version: required: false - default: 2.32.3 + default: 2.33.1 description: "Docker Compose version" runs: From ab0df5a91e1c21fb6f8192ef44e1de7492b235b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:28:56 +0100 Subject: [PATCH 154/287] build(deps): bump getsentry/action-release from 1 to 3 (#3599) Bumps [getsentry/action-release](https://github.com/getsentry/action-release) from 1 to 3. - [Release notes](https://github.com/getsentry/action-release/releases) - [Changelog](https://github.com/getsentry/action-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/getsentry/action-release/compare/v1...v3) --- updated-dependencies: - dependency-name: getsentry/action-release dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 978be3c9a62..6afafad076d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: getsentry/action-release@v1 + - uses: getsentry/action-release@v3 env: SENTRY_ORG: self-hosted SENTRY_PROJECT: installer From 9b6c1cf6587f4ed699245c68532a4bb07cad8507 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:36:24 +0000 Subject: [PATCH 155/287] build(deps): bump actions/create-github-app-token from 1.11.3 to 1.11.6 (#3598) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.3 to 1.11.6. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7...21cfef2b496dd8ef5b904c159339626a10ad380e) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6afafad076d..d38758b8c9e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3 + uses: actions/create-github-app-token@21cfef2b496dd8ef5b904c159339626a10ad380e # v1.11.6 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From d885dd331ff1991b95d285969ef2bca5add94ce0 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Mon, 3 Mar 2025 16:46:31 +0330 Subject: [PATCH 156/287] Use docker-compose if version is gte docker compose (#3595) Fixes #3587 This PR tries to use docker-compose if its version is greater than docker compose. --- install/_lib.sh | 6 ++++++ install/check-minimum-requirements.sh | 12 ------------ install/dc-detect-version.sh | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/install/_lib.sh b/install/_lib.sh index 73ef0237df9..ee7bb435203 100644 --- a/install/_lib.sh +++ b/install/_lib.sh @@ -53,6 +53,12 @@ function ensure_file_from_example { fi } +# Check the version of $1 is greater than or equal to $2 using sort. Note: versions must be stripped of "v" +function vergte() { + printf "%s\n%s" $1 $2 | sort --version-sort --check=quiet --reverse + echo $? +} + SENTRY_CONFIG_PY=sentry/sentry.conf.py SENTRY_CONFIG_YML=sentry/config.yml diff --git a/install/check-minimum-requirements.sh b/install/check-minimum-requirements.sh index 47848cc2859..fa275882721 100644 --- a/install/check-minimum-requirements.sh +++ b/install/check-minimum-requirements.sh @@ -2,12 +2,6 @@ echo "${_group}Checking minimum requirements ..." source install/_min-requirements.sh -# Check the version of $1 is greater than or equal to $2 using sort. Note: versions must be stripped of "v" -function vergte() { - printf "%s\n%s" $1 $2 | sort --version-sort --check=quiet --reverse - echo $? -} - DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' || echo '') if [[ -z "$DOCKER_VERSION" ]]; then echo "FAIL: Unable to get docker version, is the docker daemon running?" @@ -20,12 +14,6 @@ if [[ "$(vergte ${DOCKER_VERSION//v/} $MIN_DOCKER_VERSION)" -eq 1 ]]; then fi echo "Found Docker version $DOCKER_VERSION" -COMPOSE_VERSION=$($dc_base version --short || echo '') -if [[ -z "$COMPOSE_VERSION" ]]; then - echo "FAIL: Docker compose is required to run self-hosted" - exit 1 -fi - if [[ "$(vergte ${COMPOSE_VERSION//v/} $MIN_COMPOSE_VERSION)" -eq 1 ]]; then echo "FAIL: Expected minimum $dc_base version to be $MIN_COMPOSE_VERSION but found $COMPOSE_VERSION" exit 1 diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index ccee1b5eebe..a27dd7b84a6 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -10,6 +10,23 @@ echo "${_group}Initializing Docker Compose ..." # To support users that are symlinking to docker-compose dc_base="$(docker compose version &>/dev/null && echo 'docker compose' || echo 'docker-compose')" +dc_base_standalone="$(docker-compose version &>/dev/null && echo 'docker-compose' || echo '')" + +COMPOSE_VERSION=$($dc_base version --short || echo '') +STANDALONE_COMPOSE_VERSION=$($dc_base_standalone version --short &>/dev/null || echo '') + +if [[ -z "$COMPOSE_VERSION" && -z "$STANDALONE_COMPOSE_VERSION" ]]; then + echo "FAIL: Docker Compose is required to run self-hosted" + exit 1 +fi + +if [[ ! -z "${STANDALONE_COMPOSE_VERSION}" ]]; then + if [[ "$(vergte ${COMPOSE_VERSION//v/} ${STANDALONE_COMPOSE_VERSION//v/})" -eq 1 ]]; then + COMPOSE_VERSION="${STANDALONE_COMPOSE_VERSION}" + dc_base="$dc_base_standalone" + fi +fi + if [[ "$(basename $0)" = "install.sh" ]]; then dc="$dc_base --ansi never --env-file ${_ENV}" else From e86d18514597b27784bb86a6df3fc5e4ef4ce119 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Wed, 5 Mar 2025 20:25:50 +0000 Subject: [PATCH 157/287] ref: Less complicated docker compose detection (#3604) With #3595, we now check both `docker-compose` and `docker compose` versions so this patch removes the implicit fallback to `docker-compose` for `$dc_base` and makes it explicit. --- install/dc-detect-version.sh | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index a27dd7b84a6..9d52a42ad7b 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -9,22 +9,20 @@ fi echo "${_group}Initializing Docker Compose ..." # To support users that are symlinking to docker-compose -dc_base="$(docker compose version &>/dev/null && echo 'docker compose' || echo 'docker-compose')" +dc_base="$(docker compose version --short &>/dev/null && echo 'docker compose' || echo '')" dc_base_standalone="$(docker-compose version &>/dev/null && echo 'docker-compose' || echo '')" -COMPOSE_VERSION=$($dc_base version --short || echo '') -STANDALONE_COMPOSE_VERSION=$($dc_base_standalone version --short &>/dev/null || echo '') +COMPOSE_VERSION=$([ -n "$dc_base" ] && $dc_base version --short || echo '') +STANDALONE_COMPOSE_VERSION=$([ -n "$dc_base_standalone" ] && $dc_base_standalone version --short &>/dev/null || echo '') if [[ -z "$COMPOSE_VERSION" && -z "$STANDALONE_COMPOSE_VERSION" ]]; then echo "FAIL: Docker Compose is required to run self-hosted" exit 1 fi -if [[ ! -z "${STANDALONE_COMPOSE_VERSION}" ]]; then - if [[ "$(vergte ${COMPOSE_VERSION//v/} ${STANDALONE_COMPOSE_VERSION//v/})" -eq 1 ]]; then - COMPOSE_VERSION="${STANDALONE_COMPOSE_VERSION}" - dc_base="$dc_base_standalone" - fi +if [[ -z "$COMPOSE_VERSION" || -n "$STANDALONE_COMPOSE_VERSION" && "$(vergte ${COMPOSE_VERSION//v/} ${STANDALONE_COMPOSE_VERSION//v/})" -eq 1 ]]; then + COMPOSE_VERSION="${STANDALONE_COMPOSE_VERSION}" + dc_base="$dc_base_standalone" fi if [[ "$(basename $0)" = "install.sh" ]]; then From d08a6d9b668358957b7dd7712ba08a1b08aa00f8 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Thu, 6 Mar 2025 14:15:47 +0330 Subject: [PATCH 158/287] Add --short to docker-compose version (#3605) --- install/dc-detect-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index 9d52a42ad7b..ff4c9f82bf8 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -10,7 +10,7 @@ echo "${_group}Initializing Docker Compose ..." # To support users that are symlinking to docker-compose dc_base="$(docker compose version --short &>/dev/null && echo 'docker compose' || echo '')" -dc_base_standalone="$(docker-compose version &>/dev/null && echo 'docker-compose' || echo '')" +dc_base_standalone="$(docker-compose version --short &>/dev/null && echo 'docker-compose' || echo '')" COMPOSE_VERSION=$([ -n "$dc_base" ] && $dc_base version --short || echo '') STANDALONE_COMPOSE_VERSION=$([ -n "$dc_base_standalone" ] && $dc_base_standalone version --short &>/dev/null || echo '') From 979f219355c5022cabb022be5b6daa53c2a32af9 Mon Sep 17 00:00:00 2001 From: Brett Higgins Date: Thu, 6 Mar 2025 05:46:16 -0500 Subject: [PATCH 159/287] Fix unbound variable error in install script (#3601) --- install/_lib.sh | 1 - install/check-minimum-requirements.sh | 4 ++-- install/dc-detect-version.sh | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/install/_lib.sh b/install/_lib.sh index ee7bb435203..e4ce0aaaa4f 100644 --- a/install/_lib.sh +++ b/install/_lib.sh @@ -56,7 +56,6 @@ function ensure_file_from_example { # Check the version of $1 is greater than or equal to $2 using sort. Note: versions must be stripped of "v" function vergte() { printf "%s\n%s" $1 $2 | sort --version-sort --check=quiet --reverse - echo $? } SENTRY_CONFIG_PY=sentry/sentry.conf.py diff --git a/install/check-minimum-requirements.sh b/install/check-minimum-requirements.sh index fa275882721..e06db42c48a 100644 --- a/install/check-minimum-requirements.sh +++ b/install/check-minimum-requirements.sh @@ -8,13 +8,13 @@ if [[ -z "$DOCKER_VERSION" ]]; then exit 1 fi -if [[ "$(vergte ${DOCKER_VERSION//v/} $MIN_DOCKER_VERSION)" -eq 1 ]]; then +if ! vergte ${DOCKER_VERSION//v/} $MIN_DOCKER_VERSION; then echo "FAIL: Expected minimum docker version to be $MIN_DOCKER_VERSION but found $DOCKER_VERSION" exit 1 fi echo "Found Docker version $DOCKER_VERSION" -if [[ "$(vergte ${COMPOSE_VERSION//v/} $MIN_COMPOSE_VERSION)" -eq 1 ]]; then +if ! vergte ${COMPOSE_VERSION//v/} $MIN_COMPOSE_VERSION; then echo "FAIL: Expected minimum $dc_base version to be $MIN_COMPOSE_VERSION but found $COMPOSE_VERSION" exit 1 fi diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index ff4c9f82bf8..6f1b1df1a5d 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -20,7 +20,7 @@ if [[ -z "$COMPOSE_VERSION" && -z "$STANDALONE_COMPOSE_VERSION" ]]; then exit 1 fi -if [[ -z "$COMPOSE_VERSION" || -n "$STANDALONE_COMPOSE_VERSION" && "$(vergte ${COMPOSE_VERSION//v/} ${STANDALONE_COMPOSE_VERSION//v/})" -eq 1 ]]; then +if [[ -z "$COMPOSE_VERSION" ]] || [[ -n "$STANDALONE_COMPOSE_VERSION" ]] && ! vergte ${COMPOSE_VERSION//v/} ${STANDALONE_COMPOSE_VERSION//v/}; then COMPOSE_VERSION="${STANDALONE_COMPOSE_VERSION}" dc_base="$dc_base_standalone" fi From 4fff2e39df69bf6556d0df462c3a57c6d4c8c2c9 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Thu, 6 Mar 2025 14:24:25 +0330 Subject: [PATCH 160/287] Enforce license compliance only on getsentry repository (#3606) It fails every time and it isn't needed. https://github.com/aminvakil/self-hosted/actions/runs/13685684000 ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- .github/workflows/enforce-license-compliance.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index 46d724d32c4..02ca9d8e3f6 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -8,6 +8,7 @@ on: jobs: enforce-license-compliance: + if: github.repository_owner == 'getsentry' runs-on: ubuntu-latest steps: - name: 'Enforce License Compliance' From 9486a832eb80cb2833ea6fa0cbef60c4f1a2bfa9 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 10 Mar 2025 20:44:40 +0000 Subject: [PATCH 161/287] feat: provide monitoring-related configurations (#3611) In accordance with https://github.com/getsentry/sentry-docs/pull/12660 --- docker-compose.yml | 4 ++++ relay/config.example.yml | 11 +++++++++++ sentry/sentry.conf.example.py | 18 ++++++++++++++++++ symbolicator/config.example.yml | 7 +++++++ 4 files changed, 40 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 9d8ee5a9e4c..75a86e2e59b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,6 +84,10 @@ x-snuba-defaults: &snuba_defaults # Leaving the value empty to just pass whatever is set # on the host system (or in the .env file) SENTRY_EVENT_RETENTION_DAYS: + # If you have statsd server, you can utilize that to monitor self-hosted Snuba containers. + # To start, state these environment variables below on your `.env.` file and adjust the options as needed. + SNUBA_STATSD_HOST: # Example value: "100.100.123.123". Must be an IP address, not domain name + SNUBA_STATSD_PORT: # Example value: 8125 services: smtp: <<: *restart_policy diff --git a/relay/config.example.yml b/relay/config.example.yml index f73e45acca8..f1a239de495 100644 --- a/relay/config.example.yml +++ b/relay/config.example.yml @@ -18,3 +18,14 @@ processing: # # health: # max_memory_percent: 1.0 + +# If you have statsd server, you can utilize that to monitor self-hosted Relay. +# To start, uncomment the following line and adjust the options as needed. +# +# metrics: +# statsd: "100.100.123.123:8125" # It is recommended to use IP address instead of domain name +# prefix: "sentry.relay" # Adjust this to your needs, default is "sentry.relay" +# sample_rate: 1.0 # Adjust this to your needs, default is 1.0 +# # `periodic_secs` is the interval for periodic metrics emitted from Relay. +# # Setting it to `0` seconds disables the periodic metrics. +# periodic_secs: 5 diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 98e7a6c0fb3..78bd392d116 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -396,3 +396,21 @@ def get_internal_network(): # to allow specific hosts. It might be IP addresses or domain names (without `http://` or `https://`). # SENTRY_OPTIONS["relay.span-normalization.allowed_hosts"] = ["example.com", "192.168.10.1"] + +############## +# Monitoring # +############## + +# By default, Sentry uses dummy statsd monitoring backend that is a no-op. +# If you have a statsd server, you can utilize that to monitor self-hosted +# Sentry for "sentry"-related containers. +# +# To start, uncomment the following line and adjust the options as needed. + +# SENTRY_METRICS_BACKEND = 'sentry.metrics.statsd.StatsdMetricsBackend' +# SENTRY_METRICS_OPTIONS: dict[str, Any] = { +# 'host': '100.100.123.123', # It is recommended to use IP address instead of domain name +# 'port': 8125, +# } +# SENTRY_METRICS_SAMPLE_RATE = 1.0 # Adjust this to your needs, default is 1.0 +# SENTRY_METRICS_PREFIX = "sentry." # Adjust this to your needs, default is "sentry." diff --git a/symbolicator/config.example.yml b/symbolicator/config.example.yml index 62cf9b83b70..de716de8c4c 100644 --- a/symbolicator/config.example.yml +++ b/symbolicator/config.example.yml @@ -6,3 +6,10 @@ logging: metrics: statsd: null sentry_dsn: null # TODO: Automatically fill this with the internal project DSN + +# If you have statsd server, you can utilize that to monitor self-hosted Symbolicator. +# To start, uncomment the following line and adjust the options as needed. +# +# metrics: +# statsd: "100.100.123.123:8125" # It is recommended to use IP address instead of domain name +# prefix: "sentry.symbolicator" # Adjust this to your needs, default is "symbolicator" From 30f8d0125f919e2cfef2b47edf80a9b0611b74c6 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 14 Mar 2025 13:46:30 +0000 Subject: [PATCH 162/287] feat(features): enable trace view (#3617) --- sentry/sentry.conf.example.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 78bd392d116..121ac58bb13 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -298,6 +298,7 @@ def get_internal_network(): "organizations:dashboards-rh-widget", "organizations:metrics-extraction", "organizations:transaction-metrics-extraction", + "organizations:trace-view-v1", "projects:custom-inbound-filters", "projects:data-forwarding", "projects:discard-groups", From 09532da147cd032fa36f6634d56d27897308e1ea Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sat, 15 Mar 2025 18:05:53 +0000 Subject: [PATCH 163/287] release: 25.3.0 --- .env | 10 +++++----- CHANGELOG.md | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5688114306b..df49ce13784 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.3.0 +SNUBA_IMAGE=getsentry/snuba:25.3.0 +RELAY_IMAGE=getsentry/relay:25.3.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.3.0 +VROOM_IMAGE=getsentry/vroom:25.3.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 29741bb05c4..202a7b2186d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 25.3.0 + +### Various fixes & improvements + +- feat(features): enable trace view (#3617) by @aldy505 +- feat: provide monitoring-related configurations (#3611) by @aldy505 +- Enforce license compliance only on getsentry repository (#3606) by @aminvakil +- Fix unbound variable error in install script (#3601) by @brettdh +- Add --short to docker-compose version (#3605) by @aminvakil +- ref: Less complicated docker compose detection (#3604) by @BYK +- Use docker-compose if version is gte docker compose (#3595) by @aminvakil +- build(deps): bump actions/create-github-app-token from 1.11.3 to 1.11.6 (#3598) by @dependabot +- build(deps): bump getsentry/action-release from 1 to 3 (#3599) by @dependabot +- Bump docker-compose 2.33.1 (#3597) by @aminvakil +- refactor: move system.url-prefix under systems settings section (#3588) by @leeoocca + ## 25.2.0 ### Various fixes & improvements From d63138d8b70a33c7109cb7926718834abe119403 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 17 Mar 2025 14:33:46 +0000 Subject: [PATCH 164/287] feat(features): enable session replay canvas (#3619) --- sentry/sentry.conf.example.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 121ac58bb13..83fe144d896 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -290,6 +290,8 @@ def get_internal_network(): "organizations:performance-view", "organizations:advanced-search", "organizations:session-replay", + "organizations:session-replay-enable-canvas", + "organizations:session-replay-enable-canvas-replayer", "organizations:issue-platform", "organizations:profiling", "organizations:monitors", From b0c3090b176f31779dfd20d43036b94db2f86001 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 17 Mar 2025 17:39:00 +0000 Subject: [PATCH 165/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index df49ce13784..5688114306b 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.3.0 -SNUBA_IMAGE=getsentry/snuba:25.3.0 -RELAY_IMAGE=getsentry/relay:25.3.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.3.0 -VROOM_IMAGE=getsentry/vroom:25.3.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From d350bd4b7d14b51df2db1782ccae3655732c4143 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 18 Mar 2025 12:26:48 +0000 Subject: [PATCH 166/287] fix: js-sdk directory/file permission should be set correctly (#3616) --- _unit-test/js-sdk-assets-test.sh | 13 ++++++++++--- install/setup-js-sdk-assets.sh | 10 ++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/_unit-test/js-sdk-assets-test.sh b/_unit-test/js-sdk-assets-test.sh index bd898acf6a7..7e2306ecb97 100755 --- a/_unit-test/js-sdk-assets-test.sh +++ b/_unit-test/js-sdk-assets-test.sh @@ -20,14 +20,21 @@ echo $total_directories test "5" == "$total_directories" echo "Pass" -# `sdk_tree` should output "5 directories, 17 files" +# `sdk_tree` should output "6 directories, 23 files" echo "$sdk_tree" -test "5 directories, 17 files" == "$(echo "$sdk_tree")" +test "6 directories, 23 files" == "$(echo "$sdk_tree")" echo "Pass" # Files should all be >1k (ensure they are not empty) echo "Testing file sizes" -test "17" == "$non_empty_file_count" +test "23" == "$non_empty_file_count" +echo "Pass" + +# Files should be owned by the root user +echo "Testing file ownership" +directory_owners=$(echo "$sdk_files" | awk '$3=="root" { print $0 }' | wc -l) +echo "$directory_owners" +test "$directory_owners" == "8" echo "Pass" report_success diff --git a/install/setup-js-sdk-assets.sh b/install/setup-js-sdk-assets.sh index 50b9428dd4b..51c5e408191 100644 --- a/install/setup-js-sdk-assets.sh +++ b/install/setup-js-sdk-assets.sh @@ -27,14 +27,20 @@ if [[ "${SETUP_JS_SDK_ASSETS:-}" == "1" ]]; then latest_js_v6=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("6.")))) | .[0]') latest_js_v7=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("7.")))) | .[0]') latest_js_v8=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("8.")))) | .[0]') + latest_js_v9=$(echo "$loader_registry" | $jq -r '.versions | reverse | map(select(.|any(.; startswith("9.")))) | .[0]') - echo "Found JS SDKs: v${latest_js_v4}, v${latest_js_v5}, v${latest_js_v6}, v${latest_js_v7}, v${latest_js_v8}" + echo "Found JS SDKs: v${latest_js_v4}, v${latest_js_v5}, v${latest_js_v6}, v${latest_js_v7}, v${latest_js_v8}, v${latest_js_v9}" - versions="{$latest_js_v4,$latest_js_v5,$latest_js_v6,$latest_js_v7,$latest_js_v8}" + versions="{$latest_js_v4,$latest_js_v5,$latest_js_v6,$latest_js_v7,$latest_js_v8,$latest_js_v9}" variants="{bundle,bundle.tracing,bundle.tracing.replay,bundle.replay,bundle.tracing.replay.feedback,bundle.feedback}" # Download those versions & variants using curl $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx curl -w '%{response_code} %{url}\n' --no-progress-meter --compressed --retry 3 --create-dirs -fLo "/var/www/js-sdk/#1/#2.min.js" "https://browser.sentry-cdn.com/$%7Bversions%7D/$%7Bvariants%7D.min.js" || true + # Make sure permissions are correct + # See https://github.com/getsentry/self-hosted/issues/3614 for reported issue + $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk -type d -exec chmod 755 {} \; + $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk -type f -exec chmod 644 {} \; + echo "${_endgroup}" fi From 1c3047fa8fdfe0dfc6916ae610f3f56500abf685 Mon Sep 17 00:00:00 2001 From: Junsung Cho <97071544+junsung-cho@users.noreply.github.com> Date: Tue, 18 Mar 2025 21:35:07 +0900 Subject: [PATCH 167/287] docs(config): add example config for Google Auth (#3623) --- sentry/config.example.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sentry/config.example.yml b/sentry/config.example.yml index 0764183a199..54fe0c9c813 100644 --- a/sentry/config.example.yml +++ b/sentry/config.example.yml @@ -133,3 +133,12 @@ transaction-events.force-disable-internal-project: true # discord.public-key: "" # discord.client-secret: "" # discord.bot-token: "" + +############### +# Google Auth # +############### + +# Refer to https://develop.sentry.dev/self-hosted/sso/#google-auth + +# auth-google.client-id: "" +# auth-google.client-secret: "" From 0970b144f582b2008635631c8f95ef5e1e89b9b7 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 24 Mar 2025 14:09:06 +0000 Subject: [PATCH 168/287] feat(sentry): add dynamic sampling feature to config (#3631) --- sentry/sentry.conf.example.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 83fe144d896..b1d5a8daef5 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -301,6 +301,7 @@ def get_internal_network(): "organizations:metrics-extraction", "organizations:transaction-metrics-extraction", "organizations:trace-view-v1", + "organizations:dynamic-sampling", "projects:custom-inbound-filters", "projects:data-forwarding", "projects:discard-groups", From c32836714d736dae6db3ba9c38e6beb82735a90a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:14:17 +0000 Subject: [PATCH 169/287] build(deps): bump actions/create-github-app-token from 1.11.6 to 1.11.7 (#3632) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.6 to 1.11.7. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/21cfef2b496dd8ef5b904c159339626a10ad380e...af35edadc00be37caa72ed9f3e6d5f7801bfdf09) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d38758b8c9e..390ac8e7580 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@21cfef2b496dd8ef5b904c159339626a10ad380e # v1.11.6 + uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1.11.7 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From ae05091f9ff55c664ef453d121fe5c89c48a4b9d Mon Sep 17 00:00:00 2001 From: Kliachin Aleksei Date: Thu, 27 Mar 2025 12:14:02 +0300 Subject: [PATCH 170/287] Minimum requirements for 'errors-only' profile (#3634) Using the [errors-only](https://develop.sentry.dev/self-hosted/experimental/errors-only/) profile, fewer resources are required. About 2 times. --- install/_min-requirements.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/install/_min-requirements.sh b/install/_min-requirements.sh index c518508e2f3..17afa00995d 100644 --- a/install/_min-requirements.sh +++ b/install/_min-requirements.sh @@ -4,6 +4,10 @@ MIN_COMPOSE_VERSION='2.32.2' # 16 GB minimum host RAM, but there'll be some overhead outside of what # can be allotted to docker -MIN_RAM_HARD=14000 # MB - -MIN_CPU_HARD=4 +if [[ "$COMPOSE_PROFILES" == "errors-only" ]]; then + MIN_RAM_HARD=7000 # MB + MIN_CPU_HARD=2 +else + MIN_RAM_HARD=14000 # MB + MIN_CPU_HARD=4 +fi From b2819cdac13baa04e6212d0551ed718a79227bb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 09:09:52 +0300 Subject: [PATCH 171/287] build(deps): bump actions/create-github-app-token from 1.11.7 to 1.12.0 (#3639) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.11.7 to 1.12.0. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/af35edadc00be37caa72ed9f3e6d5f7801bfdf09...d72941d797fd3113feb6b93fd0dec494b13a2547) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 390ac8e7580..d07dd657304 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1.11.7 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 53412bbefbc09a076a0beb52c10efa0589a6b52e Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Sat, 12 Apr 2025 18:59:38 +0330 Subject: [PATCH 172/287] Fix STANDALONE_COMPOSE_VERSION variable setting (#3654) --- install/dc-detect-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index 6f1b1df1a5d..16297363362 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -13,7 +13,7 @@ dc_base="$(docker compose version --short &>/dev/null && echo 'docker compose' | dc_base_standalone="$(docker-compose version --short &>/dev/null && echo 'docker-compose' || echo '')" COMPOSE_VERSION=$([ -n "$dc_base" ] && $dc_base version --short || echo '') -STANDALONE_COMPOSE_VERSION=$([ -n "$dc_base_standalone" ] && $dc_base_standalone version --short &>/dev/null || echo '') +STANDALONE_COMPOSE_VERSION=$([ -n "$dc_base_standalone" ] && $dc_base_standalone version --short || echo '') if [[ -z "$COMPOSE_VERSION" && -z "$STANDALONE_COMPOSE_VERSION" ]]; then echo "FAIL: Docker Compose is required to run self-hosted" From 6b4487f032e53a7e18217501956c133b2859e7f1 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Sat, 12 Apr 2025 19:00:25 +0330 Subject: [PATCH 173/287] Use dc variable in clickhouse step (#3658) --- install/upgrade-clickhouse.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/upgrade-clickhouse.sh b/install/upgrade-clickhouse.sh index bd69d7bf746..3dcb56f189c 100644 --- a/install/upgrade-clickhouse.sh +++ b/install/upgrade-clickhouse.sh @@ -1,7 +1,7 @@ echo "${_group}Upgrading Clickhouse ..." # First check to see if user is upgrading by checking for existing clickhouse volume -if docker compose ps -a | grep -q clickhouse; then +if $dc ps -a | grep -q clickhouse; then # Start clickhouse if it is not already running $dc up --wait clickhouse From a956339180f3ce06ffec59a83594eafc231a4a7a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Apr 2025 18:32:35 +0300 Subject: [PATCH 174/287] build(deps): bump actions/create-github-app-token from 1.12.0 to 2.0.2 (#3649) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 1.12.0 to 2.0.2. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/d72941d797fd3113feb6b93fd0dec494b13a2547...3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 2.0.2 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d07dd657304..3a72ed938f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 054b9155f0ae1b88dc4f5095231b014b6a8ff252 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 12 Apr 2025 15:42:08 +0000 Subject: [PATCH 175/287] chore(relay): specify spool.enveloppe.max_backpressure_memory_percent configuration for handling relay's failing healthcheck (#3635) * chore(relay): specify spool.enveloppe.max_backpressure_memory_percent configuration for handling relay's failing healthcheck Although a fix is being rolled out, that does not mean every relay instance would suddenly be fixed. We would need to still provide a workaround for people to try out. Refer to this specific issue comment: https://github.com/getsentry/self-hosted/issues/3330#issuecomment-2751092219 * Update config.example.yml Co-authored-by: Riccardo Busetti * chore: default path for relay spool envelopes --------- Co-authored-by: Riccardo Busetti --- relay/config.example.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/relay/config.example.yml b/relay/config.example.yml index f1a239de495..06c73079564 100644 --- a/relay/config.example.yml +++ b/relay/config.example.yml @@ -14,13 +14,18 @@ processing: # In some cases, relay might fail to find out the actual machine memory # therefore it makes the healthcheck fail and events can't be submitted. -# As a workaround, uncomment the following line: +# See https://github.com/getsentry/self-hosted/issues/3330 for more details. +# As a workaround, uncomment the following `health` and `spool` sections: # # health: # max_memory_percent: 1.0 +# spool: +# envelopes: +# path: "/tmp/relay-spool-envelopes" +# max_backpressure_memory_percent: 1.0 # If you have statsd server, you can utilize that to monitor self-hosted Relay. -# To start, uncomment the following line and adjust the options as needed. +# To start, uncomment the following `metrics` section and adjust the options as needed. # # metrics: # statsd: "100.100.123.123:8125" # It is recommended to use IP address instead of domain name From 36d8b2c6b2571d3f36a8bf7508b5bd57740bd4f2 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 15 Apr 2025 18:06:13 +0000 Subject: [PATCH 176/287] release: 25.4.0 --- .env | 10 +++++----- CHANGELOG.md | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5688114306b..6eb15751fa6 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.4.0 +SNUBA_IMAGE=getsentry/snuba:25.4.0 +RELAY_IMAGE=getsentry/relay:25.4.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.4.0 +VROOM_IMAGE=getsentry/vroom:25.4.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 202a7b2186d..b5236ce1d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## 25.4.0 + +### Stand-alone Docker Compose Fixes + +By: @aminvakil (#3658, #3654) + +### Various fixes & improvements + +- chore(relay): specify spool.enveloppe.max_backpressure_memory_percent configuration for handling relay's failing healthcheck (#3635) by @aldy505 +- build(deps): bump actions/create-github-app-token from 1.12.0 to 2.0.2 (#3649) by @dependabot +- build(deps): bump actions/create-github-app-token from 1.11.7 to 1.12.0 (#3639) by @dependabot +- Minimum requirements for 'errors-only' profile (#3634) by @madest92 +- build(deps): bump actions/create-github-app-token from 1.11.6 to 1.11.7 (#3632) by @dependabot +- feat(sentry): add dynamic sampling feature to config (#3631) by @aldy505 +- docs(config): add example config for Google Auth (#3623) by @junsung-cho +- fix: js-sdk directory/file permission should be set correctly (#3616) by @aldy505 +- feat(features): enable session replay canvas (#3619) by @aldy505 + ## 25.3.0 ### Various fixes & improvements From 92b419b551e8e6b0da2561d480fb8b02b98488e7 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 15 Apr 2025 22:13:08 +0000 Subject: [PATCH 177/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 6eb15751fa6..5688114306b 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.4.0 -SNUBA_IMAGE=getsentry/snuba:25.4.0 -RELAY_IMAGE=getsentry/relay:25.4.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.4.0 -VROOM_IMAGE=getsentry/vroom:25.4.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 241b58dd2229a4f3837286ffe6e05918b50729ac Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Thu, 1 May 2025 13:18:23 -0400 Subject: [PATCH 178/287] ref: remove SENTRY_USE_BIG_INTS (always True) (#3687) --- sentry/sentry.conf.example.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index b1d5a8daef5..d97b50b9ce8 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -53,10 +53,6 @@ def get_internal_network(): } } -# You should not change this setting after your database has been created -# unless you have altered all schemas first -SENTRY_USE_BIG_INTS = True - # If you're expecting any kind of real traffic on Sentry, we highly recommend # configuring the CACHES and Redis settings From 84094424e8834002266bfa33ba5fdec75635e6c0 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Fri, 2 May 2025 14:19:34 +0300 Subject: [PATCH 179/287] Resolve datetime deprecation warnings (#3686) # PR Summary This small PR fixes the `datetime` deprecation warnings which you can find in the CI logs: ```python /home/runner/work/_actions/getsentry/self-hosted/master/_integration-test/test_01_basics.py:303: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). ``` --- _integration-test/test_01_basics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index 110de083855..121aded1b18 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -177,8 +177,8 @@ def test_custom_certificate_authorities(): .issuer_name(ca_name) .public_key(ca_key.public_key()) .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.datetime.utcnow()) - .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=1)) + .not_valid_before(datetime.datetime.now(datetime.timezone.utc)) + .not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1)) .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) .add_extension( x509.KeyUsage( From f7b6c03346ecd8eec945e372775cbea356f85b2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 09:12:26 -0700 Subject: [PATCH 180/287] build(deps): bump actions/create-github-app-token from 2.0.2 to 2.0.6 (#3690) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.0.2 to 2.0.6. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5...df432ceedc7162793a195dd1713ff69aefc7379e) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 2.0.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a72ed938f1..d744f50290a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 + uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From f183e71bab45a0dd027c9535d7c5e79fe61a9ba7 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 15 May 2025 18:07:10 +0000 Subject: [PATCH 181/287] release: 25.5.0 --- .env | 10 +++++----- CHANGELOG.md | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5688114306b..629512829c0 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.5.0 +SNUBA_IMAGE=getsentry/snuba:25.5.0 +RELAY_IMAGE=getsentry/relay:25.5.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.5.0 +VROOM_IMAGE=getsentry/vroom:25.5.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index b5236ce1d4a..9286ecbd85c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 25.5.0 + +### Various fixes & improvements + +- build(deps): bump actions/create-github-app-token from 2.0.2 to 2.0.6 (#3690) by @dependabot +- Resolve datetime deprecation warnings (#3686) by @emmanuel-ferdman +- ref: remove SENTRY_USE_BIG_INTS (always True) (#3687) by @asottile-sentry + ## 25.4.0 ### Stand-alone Docker Compose Fixes From b8b0ee80e7b92556202dfc49ef2e7d055d302781 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 15 May 2025 18:50:41 +0000 Subject: [PATCH 182/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 629512829c0..5688114306b 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.5.0 -SNUBA_IMAGE=getsentry/snuba:25.5.0 -RELAY_IMAGE=getsentry/relay:25.5.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.5.0 -VROOM_IMAGE=getsentry/vroom:25.5.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 9e085a0f7891c22a52a90e76f0ced0bcd79ef957 Mon Sep 17 00:00:00 2001 From: Nikita Korolev <66738864+doc-sheet@users.noreply.github.com> Date: Fri, 16 May 2025 12:53:41 +0300 Subject: [PATCH 183/287] chore: cleanup obsolete feature flags (#3701) * remove obsolete SENTRY_RELEASE_HEALTH removed in https://github.com/getsentry/sentry/pull/68226 * remove unused feature flags removed in https://github.com/getsentry/sentry/pull/32010 * remove session-replay-enable-canvas removed in https://github.com/getsentry/sentry/pull/87762 --- sentry/sentry.conf.example.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index d97b50b9ce8..902c393fcf2 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -196,15 +196,6 @@ def get_internal_network(): SENTRY_DIGESTS = "sentry.digests.backends.redis.RedisBackend" -################### -# Metrics Backend # -################### - -SENTRY_RELEASE_HEALTH = "sentry.release_health.metrics.MetricsReleaseHealthBackend" -SENTRY_RELEASE_MONITOR = ( - "sentry.release_health.release_monitor.metrics.MetricReleaseMonitorBackend" -) - ############## # Web Server # ############## @@ -273,21 +264,17 @@ def get_internal_network(): feature: True for feature in ( "organizations:discover", - "organizations:events", "organizations:global-views", "organizations:incidents", "organizations:integrations-issue-basic", "organizations:integrations-issue-sync", "organizations:invite-members", - "organizations:metric-alert-builder-aggregate", "organizations:sso-basic", "organizations:sso-rippling", "organizations:sso-saml2", "organizations:performance-view", "organizations:advanced-search", "organizations:session-replay", - "organizations:session-replay-enable-canvas", - "organizations:session-replay-enable-canvas-replayer", "organizations:issue-platform", "organizations:profiling", "organizations:monitors", From 031a1f10937f3e49219d19474ac2fcb2d1c474a1 Mon Sep 17 00:00:00 2001 From: Dominik Jakielski <52488859+djakielski@users.noreply.github.com> Date: Mon, 19 May 2025 10:24:57 +0200 Subject: [PATCH 184/287] Add missing lib script to sentry-admin.sh (#3693) Sentry Admin Script always fail because of missing import of lib script. ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms. --- install.sh | 9 +++++++-- install/_lib.sh | 10 ---------- install/_logging.sh | 3 +++ install/dc-detect-version.sh | 2 +- sentry-admin.sh | 1 + 5 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 install/_logging.sh diff --git a/install.sh b/install.sh index 23726ce97fd..86e4de1b404 100755 --- a/install.sh +++ b/install.sh @@ -1,12 +1,17 @@ #!/usr/bin/env bash -set -eE +set -eEuo pipefail +test "${DEBUG:-}" && set -x + +# Override any user-supplied umask that could cause problems, see #1222 +umask 002 # Pre-pre-flight? 🤷 -if [[ -n "$MSYSTEM" ]]; then +if [[ -n "${MSYSTEM:-}" ]]; then echo "Seems like you are using an MSYS2-based system (such as Git Bash) which is not supported. Please use WSL instead." exit 1 fi +source install/_logging.sh source install/_lib.sh # Pre-flight. No impact yet. diff --git a/install/_lib.sh b/install/_lib.sh index e4ce0aaaa4f..9abbf1a3d3b 100644 --- a/install/_lib.sh +++ b/install/_lib.sh @@ -1,13 +1,3 @@ -set -euo pipefail -test "${DEBUG:-}" && set -x - -# Override any user-supplied umask that could cause problems, see #1222 -umask 002 - -# Thanks to https://unix.stackexchange.com/a/145654/108960 -log_file=sentry_install_log-$(date +'%Y-%m-%d_%H-%M-%S').txt -exec &> >(tee -a "$log_file") - # Allow `.env` overrides using the `.env.custom` file. # We pass this to docker compose in a couple places. if [[ -f .env.custom ]]; then diff --git a/install/_logging.sh b/install/_logging.sh new file mode 100644 index 00000000000..ea529dade98 --- /dev/null +++ b/install/_logging.sh @@ -0,0 +1,3 @@ +# Thanks to https://unix.stackexchange.com/a/145654/108960 +log_file=sentry_install_log-$(date +'%Y-%m-%d_%H-%M-%S').txt +exec &> >(tee -a "$log_file") diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index 16297363362..5acaf59c676 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -34,5 +34,5 @@ proxy_args="--build-arg http_proxy=${http_proxy:-} --build-arg https_proxy=${htt dcr="$dc run --pull=never --rm" dcb="$dc build $proxy_args" dbuild="docker build $proxy_args" - +echo "$dcr" echo "${_endgroup}" diff --git a/sentry-admin.sh b/sentry-admin.sh index 386b3d57011..f90af33c81a 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -4,6 +4,7 @@ cd $(dirname $0) # Detect docker and platform state. +source install/_lib.sh source install/dc-detect-version.sh source install/detect-platform.sh From cbebc4f3f43a6ef5834d059bb144e67b7515d752 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 21 May 2025 15:46:37 +0000 Subject: [PATCH 185/287] release: 25.5.1 --- .env | 10 +++++----- CHANGELOG.md | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5688114306b..455fc7b0c2c 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.5.1 +SNUBA_IMAGE=getsentry/snuba:25.5.1 +RELAY_IMAGE=getsentry/relay:25.5.1 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.5.1 +VROOM_IMAGE=getsentry/vroom:25.5.1 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9286ecbd85c..d421410f31c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 25.5.1 + +### Various fixes & improvements + +- Add missing lib script to sentry-admin.sh (#3693) by @djakielski +- chore: cleanup obsolete feature flags (#3701) by @doc-sheet + ## 25.5.0 ### Various fixes & improvements From 5173b3197e3f2e19112eb777fd352e520955d506 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 21 May 2025 16:21:20 +0000 Subject: [PATCH 186/287] build: Set master version to nightly #skip-changelog --- .env | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 455fc7b0c2c..5688114306b 100644 --- a/.env +++ b/.env @@ -9,11 +9,11 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.5.1 -SNUBA_IMAGE=getsentry/snuba:25.5.1 -RELAY_IMAGE=getsentry/relay:25.5.1 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.5.1 -VROOM_IMAGE=getsentry/vroom:25.5.1 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 4276f44a07fbd3eda25ef5d511ca75b1f1b6be99 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 27 May 2025 13:07:44 +0200 Subject: [PATCH 187/287] Make usage of Python SDK future proof (#3714) Fixes problems that appear when Python SDK 3.0 will be released. --- _integration-test/test_01_basics.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index 121aded1b18..741d4fd807c 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -123,8 +123,10 @@ def test_login(client_login): def test_receive_event(client_login): event_id = None client, _ = client_login - with sentry_sdk.init(dsn=get_sentry_dsn(client)): - event_id = sentry_sdk.capture_exception(Exception("a failure")) + + sentry_sdk.init(dsn=get_sentry_dsn(client)) + + event_id = sentry_sdk.capture_exception(Exception("a failure")) assert event_id is not None response = poll_for_response( f"{SENTRY_TEST_HOST}/api/0/projects/sentry/internal/events/{event_id}/", client @@ -377,18 +379,19 @@ def test_custom_certificate_authorities(): def test_receive_transaction_events(client_login): client, _ = client_login - with sentry_sdk.init( + sentry_sdk.init( dsn=get_sentry_dsn(client), profiles_sample_rate=1.0, traces_sample_rate=1.0 - ): + ) + + def placeholder_fn(): + sum = 0 + for i in range(5): + sum += i + time.sleep(0.25) - def placeholder_fn(): - sum = 0 - for i in range(5): - sum += i - time.sleep(0.25) + with sentry_sdk.start_transaction(op="task", name="Test Transactions"): + placeholder_fn() - with sentry_sdk.start_transaction(op="task", name="Test Transactions"): - placeholder_fn() poll_for_response( f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=profiles&field=profile.id&project=1&statsPeriod=1h", client, From ed04842604c396e8e11d32a375208c20ec87554b Mon Sep 17 00:00:00 2001 From: anthony sottile <103459774+asottile-sentry@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:56:16 -0400 Subject: [PATCH 188/287] remove index workaround (#3730) a hard stop is in place with this so it can be removed now --- install/set-up-and-migrate-database.sh | 9 --------- 1 file changed, 9 deletions(-) diff --git a/install/set-up-and-migrate-database.sh b/install/set-up-and-migrate-database.sh index 2d4e13208e3..50822a47b75 100644 --- a/install/set-up-and-migrate-database.sh +++ b/install/set-up-and-migrate-database.sh @@ -10,15 +10,6 @@ if [[ -z "${SKIP_SENTRY_MIGRATIONS:-}" ]]; then exit 1 fi - # Using django ORM to provide broader support for users with external databases - $dcr web shell -c " -from django.db import connection - -with connection.cursor() as cursor: - cursor.execute('ALTER TABLE IF EXISTS sentry_groupedmessage DROP CONSTRAINT IF EXISTS sentry_groupedmessage_project_id_id_515aaa7e_uniq;') - cursor.execute('DROP INDEX IF EXISTS sentry_groupedmessage_project_id_id_515aaa7e_uniq;') -" - if [[ -n "${CI:-}" || "${SKIP_USER_CREATION:-0}" == 1 ]]; then $dcr web upgrade --noinput --create-kafka-topics echo "" From e684c7e3bd09e0a697442330a8600d6845528b96 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 4 Jun 2025 12:44:58 +0700 Subject: [PATCH 189/287] chore: prune removed feature flags on main repository (#3731) --- sentry/sentry.conf.example.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 902c393fcf2..2b10e77300d 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -294,20 +294,11 @@ def get_internal_network(): ) # Starfish related flags + ( - "organizations:deprecate-fid-from-performance-score", "organizations:indexed-spans-extraction", "organizations:insights-entry-points", "organizations:insights-initial-modules", "organizations:insights-addon-modules", - "organizations:mobile-ttid-ttfd-contribution", - "organizations:performance-calculate-score-relay", "organizations:standalone-span-ingestion", - "organizations:starfish-browser-resource-module-image-view", - "organizations:starfish-browser-resource-module-ui", - "organizations:starfish-browser-webvitals", - "organizations:starfish-browser-webvitals-pageoverview-v2", - "organizations:starfish-browser-webvitals-replace-fid-with-inp", - "organizations:starfish-browser-webvitals-use-backend-scores", "organizations:starfish-mobile-appstart", "projects:span-metrics-extraction", "projects:span-metrics-extraction-addons", From 1217f469ec4a7d1f8329494f7b61927f5156c28a Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Fri, 6 Jun 2025 17:07:49 -0700 Subject: [PATCH 190/287] fix(profiles): Run the profile chunks consumer (#3739) --- docker-compose.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 75a86e2e59b..ae520f7135e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -299,6 +299,11 @@ services: command: rust-consumer --storage functions_raw --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset profiles: - feature-complete + snuba-profiling-profile-chunks-consumer: + <<: *snuba_defaults + command: rust-consumer --storage profile_chunks --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + profiles: + - feature-complete snuba-spans-consumer: <<: *snuba_defaults command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset From c8ee02de19c71dd89f722060f6a7d90d0c619378 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 11 Jun 2025 10:57:24 -0400 Subject: [PATCH 191/287] feat: Add taskbroker + worker + scheduler (#3738) --- .env | 1 + .github/ISSUE_TEMPLATE/release.yml | 1 + docker-compose.yml | 22 ++++++++++++++++++++++ scripts/bump-version.sh | 2 +- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.env b/.env index 5688114306b..a73d4dd4ba7 100644 --- a/.env +++ b/.env @@ -13,6 +13,7 @@ SENTRY_IMAGE=getsentry/sentry:nightly SNUBA_IMAGE=getsentry/snuba:nightly RELAY_IMAGE=getsentry/relay:nightly SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +TASKBROKER_IMAGE=getsentry/taskbroker:nightly VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/.github/ISSUE_TEMPLATE/release.yml b/.github/ISSUE_TEMPLATE/release.yml index 48691f3255d..545e733befe 100644 --- a/.github/ISSUE_TEMPLATE/release.yml +++ b/.github/ISSUE_TEMPLATE/release.yml @@ -15,6 +15,7 @@ body: - [ ] [`snuba`](https://github.com/getsentry/snuba/actions/workflows/release.yml) - [ ] [`symbolicator`](https://github.com/getsentry/symbolicator/actions/workflows/release.yml) - [ ] [`vroom`](https://github.com/getsentry/vroom/actions/workflows/release.yaml) + - [ ] [`taskbroker`](https://github.com/getsentry/taskbroker/actions/workflows/release.yml) - [ ] Release self-hosted. - [ ] [Prepare the `self-hosted` release](https://github.com/getsentry/self-hosted/actions/workflows/release.yml) (_replace with publish issue repo link_). - [ ] Check to make sure the new release branch in self-hosted includes the appropriate CalVer images. diff --git a/docker-compose.yml b/docker-compose.yml index ae520f7135e..b1805dd3a09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -487,6 +487,24 @@ services: <<: *depends_on-healthy web: <<: *depends_on-healthy + taskbroker: + <<: *restart_policy + image: "$TASKBROKER_IMAGE" + environment: + TASKBROKER_KAFKA_CLUSTER: "kafka:9092" + TASKBROKER_KAFKA_DEADLETTER_CLUSTER: "kafka:9092" + TASKBROKER_DB_PATH: "/opt/sqlite/taskbroker-activations.sqlite" + volumes: + - sentry-taskbroker:/opt/sqlite + depends_on: + kafka: + <<: *depends_on-healthy + taskscheduler: + <<: *sentry_defaults + command: run taskworker-scheduler + taskworker: + <<: *sentry_defaults + command: run taskworker --concurrency=4 --rpc-host=taskbroker:50051 --num-brokers=1 vroom: <<: *restart_policy image: "$VROOM_IMAGE" @@ -541,6 +559,10 @@ volumes: # Not being external will still persist data across restarts. # It won't persist if someone does a docker compose down -v. sentry-vroom: + # This volume stores task data that is inflight + # It should persist across restarts. If this volume is + # deleted, up to ~2048 tasks will be lost. + sentry-taskbroker: # These store ephemeral data that needn't persist across restarts. # That said, volumes will be persisted across restarts until they are deleted. sentry-secrets: diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 808df09aa17..5e53ec81b20 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -4,7 +4,7 @@ set -eu OLD_VERSION="$1" NEW_VERSION="$2" -sed -i -e "s/^\(SENTRY\|SNUBA\|RELAY\|SYMBOLICATOR\|VROOM\)_IMAGE=\([^:]\+\):.\+\$/\1_IMAGE=\2:$NEW_VERSION/" .env +sed -i -e "s/^\(SENTRY\|SNUBA\|RELAY\|SYMBOLICATOR\|TASKBROKER\|VROOM\)_IMAGE=\([^:]\+\):.\+\$/\1_IMAGE=\2:$NEW_VERSION/" .env sed -i -e "s/^\# Self-Hosted Sentry .*/# Self-Hosted Sentry $NEW_VERSION/" README.md echo "New version: $NEW_VERSION" From 4c973e0824d5eb5352d9dfd21789227fcc5da748 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 12 Jun 2025 07:55:54 +0700 Subject: [PATCH 192/287] feat(features): enable continuous profiling (#3742) --- sentry/sentry.conf.example.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 2b10e77300d..79c7aefa6cd 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -309,6 +309,11 @@ def get_internal_network(): "organizations:user-feedback-replay-clip", "organizations:user-feedback-ui", ) + # Continuous Profiling related flags + + ( + "organizations:continuous-profiling", + "organizations:continuous-profiling-stats", + ) } ) From 2f2bb9c925c780dc456198adc288f8a1aa8a94f7 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Fri, 13 Jun 2025 10:43:39 +0200 Subject: [PATCH 193/287] tests: Install version 2.x of Python SDK (#3745) --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 62b4166f202..97d735ef800 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -sentry-sdk>=2.4.0 +sentry-sdk>=2.4.0,<3.0.0 pytest>=8.0.0 pytest-cov>=4.1.0 pytest-rerunfailures>=11.0 From 0730d8c8c7ee653b88307f860356fa827f0e9fae Mon Sep 17 00:00:00 2001 From: Nikita Korolev <66738864+doc-sheet@users.noreply.github.com> Date: Fri, 13 Jun 2025 11:52:47 +0300 Subject: [PATCH 194/287] add shellcheck action to lint bash scripts (#3710) * add shellcheck action to lint bash scripts * fix some shellcheck warnings --------- Co-authored-by: ds Co-authored-by: Burak Yigit Kaya --- .github/workflows/shellcheck.yml | 40 ++++++++++++++++++++++++++++++++ install/_lib.sh | 9 +++---- 2 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/shellcheck.yml diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 00000000000..e2d06d1ae53 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,40 @@ +--- +name: "ShellCheck" +on: + push: + paths: + - "**.sh" + branches: [master] + pull_request: + paths: + - "**.sh" + branches: [master] + +jobs: + shellcheck: + name: ShellCheck + runs-on: ubuntu-latest + steps: + - name: Repository checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run ShellCheck + run: | + git diff --name-only -z `git merge-base origin/master HEAD` -- \ + 'install/_lib.sh' \ + | xargs -0 -r -- \ + shellcheck \ + --shell=bash \ + --exclude=SC1090,SC1091 \ + --format=json1 \ + | jq -r ' + .comments + | map(.level |= if ([.] | inside(["info", "style"])) then "notice" else . end) + | .[] as $note + | "::\($note.level) file=\($note.file),line=\($note.line),endLine=\($note.endLine),col=\($note.column),endColumn=\($note.endColumn)::[SC\($note.code)] \($note.message)" + ' \ + | grep . >&2 && exit 1 + + exit 0 diff --git a/install/_lib.sh b/install/_lib.sh index 9abbf1a3d3b..3beb18e38db 100644 --- a/install/_lib.sh +++ b/install/_lib.sh @@ -33,6 +33,7 @@ function ensure_file_from_example { echo "$target already exists, skipped creation." else # sed from https://stackoverflow.com/a/25123013/90297 + # shellcheck disable=SC2001 example="$(echo "$target" | sed 's/\.[^.]*$/.example&/')" if [[ ! -f "$example" ]]; then echo "Oops! Where did $example go? 🤨 We need it in order to create $target." @@ -45,14 +46,14 @@ function ensure_file_from_example { # Check the version of $1 is greater than or equal to $2 using sort. Note: versions must be stripped of "v" function vergte() { - printf "%s\n%s" $1 $2 | sort --version-sort --check=quiet --reverse + printf "%s\n%s" "$1" "$2" | sort --version-sort --check=quiet --reverse } -SENTRY_CONFIG_PY=sentry/sentry.conf.py -SENTRY_CONFIG_YML=sentry/config.yml +export SENTRY_CONFIG_PY=sentry/sentry.conf.py +export SENTRY_CONFIG_YML=sentry/config.yml # Increase the default 10 second SIGTERM timeout # to ensure celery queues are properly drained # between upgrades as task signatures may change across # versions -STOP_TIMEOUT=60 # seconds +export STOP_TIMEOUT=60 # seconds From 1b88e90e30f1c920aa274e834c59a6d073028cec Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 13 Jun 2025 15:55:30 +0700 Subject: [PATCH 195/287] Introduce patches with external kafka (#3521) * Introduce patches with external kafka * Fix pre-commit hooks * Patch relay config file * Documentation for patches stuff * Provide more helpful information for Docker Compose Override file * Fix grep command * ref: rename to 'optional-modifications' * chore(pre-commit): exclude .patch extension * chore(pre-commit): escape backslash * chore(pre-commit): put exclude field on hooks * chore(pre-commit): put exclude field on top level Based on https://pre-commit.com/#top_level-exclude * chore(pre-commit): move to even more top level --- .pre-commit-config.yaml | 2 +- optional-modifications/README.md | 41 +++++ optional-modifications/_lib.sh | 15 ++ .../patches/external-kafka/.env.patch | 22 +++ .../external-kafka/config.example.yml.patch | 19 +++ .../external-kafka/docker-compose.yml.patch | 142 ++++++++++++++++++ .../sentry.conf.example.py.patch | 21 +++ 7 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 optional-modifications/README.md create mode 100755 optional-modifications/_lib.sh create mode 100644 optional-modifications/patches/external-kafka/.env.patch create mode 100644 optional-modifications/patches/external-kafka/config.example.yml.patch create mode 100644 optional-modifications/patches/external-kafka/docker-compose.yml.patch create mode 100644 optional-modifications/patches/external-kafka/sentry.conf.example.py.patch diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1d995372865..9d544410bab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +exclude: '\.patch$' repos: - repo: local hooks: @@ -11,7 +12,6 @@ repos: args: [-w, -d] files: .*\.sh stages: [commit, merge-commit, push, manual] - - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: diff --git a/optional-modifications/README.md b/optional-modifications/README.md new file mode 100644 index 00000000000..84cf7e0e2b9 --- /dev/null +++ b/optional-modifications/README.md @@ -0,0 +1,41 @@ +# Optional Modifications + +Other than the default self-hosted Sentry installation, sometimes users +can leverage their existing infrastructure to help them with limited +resources. "Patches", or you might call this like a "plugin system", is +a collection of patch files (see [man patch(1)](https://man7.org/linux/man-pages/man1/patch.1.html)) +that can be used with to modify the existing configuration to achieve +the desired goal. + +> [!WARNING] +> Beware that this is very experimental and might not work as expected. +> +> **Use it at your own risk!** + +## How to use patches + +The patches are designed mostly to help modify the existing +configuration files. You will need to run the `install.sh` script +afterwards. + +They should be run from the root directory. For example, the +`external-kafka` patches should be run as: + +```bash +patch < optional-modifications/patches/external-kafka/.env.patch +patch < optional-modifications/patches/external-kafka/config.example.yml.patch +patch < optional-modifications/patches/external-kafka/sentry.conf.example.py.patch +patch < optional-modifications/patches/external-kafka/docker-compose.yml.patch +``` + +Some patches might require additional steps to be taken, like providing +credentials or additional TLS certificates. + +## Official support + +Sentry employees are not obliged to provide dedicated support for +patches, but they can help by providing information to move us forward. +We encourage the community to contribute for any bug fixes or +improvements. + +See the [support policy for self-hosted Sentry](https://develop.sentry.dev/self-hosted/support/) for more information. diff --git a/optional-modifications/_lib.sh b/optional-modifications/_lib.sh new file mode 100755 index 00000000000..46e55d3e257 --- /dev/null +++ b/optional-modifications/_lib.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail +test "${DEBUG:-}" && set -x + +function patch_file() { + target="$1" + content="$2" + if [[ -f "$target" ]]; then + echo "🙈 Patching $target ..." + patch -p1 <"$content" + else + echo "🙊 Skipping $target ..." + fi +} diff --git a/optional-modifications/patches/external-kafka/.env.patch b/optional-modifications/patches/external-kafka/.env.patch new file mode 100644 index 00000000000..36e45f7785f --- /dev/null +++ b/optional-modifications/patches/external-kafka/.env.patch @@ -0,0 +1,22 @@ +--- .env 2025-02-04 07:31:54.868049984 +0700 ++++ .env.external-kafka 2025-05-15 08:33:15.442361105 +0700 +@@ -22,3 +22,19 @@ + POSTGRES_MAX_CONNECTIONS=100 + # Set SETUP_JS_SDK_ASSETS to 1 to enable the setup of JS SDK assets + # SETUP_JS_SDK_ASSETS=1 ++ ++################################################################################ ++## Additional External Kafka options ++################################################################################ ++KAFKA_BOOTSTRAP_SERVERS=kafka-node1:9092,kafka-node2:9092,kafka-node3:9092 ++# Valid options are PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL ++KAFKA_SECURITY_PROTOCOL=PLAINTEXT ++# Valid options are PLAIN, SCRAM-SHA-256, SCRAM-SHA-512. Other mechanism might be unavailable. ++# KAFKA_SASL_MECHANISM=PLAIN ++# KAFKA_SASL_USERNAME=username ++# KAFKA_SASL_PASSWORD=password ++# Put your certificates on the \`certificates/kafka\` directory. ++# The certificates will be mounted as read-only volumes. ++# KAFKA_SSL_CA_LOCATION=/kafka-certificates/ca.pem ++# KAFKA_SSL_CERTIFICATE_LOCATION=/kafka-certificates/client.pem ++# KAFKA_SSL_KEY_LOCATION=/kafka-certificates/client.key diff --git a/optional-modifications/patches/external-kafka/config.example.yml.patch b/optional-modifications/patches/external-kafka/config.example.yml.patch new file mode 100644 index 00000000000..a0c1aab04b8 --- /dev/null +++ b/optional-modifications/patches/external-kafka/config.example.yml.patch @@ -0,0 +1,19 @@ +--- relay/config.example.yml 2025-05-15 08:27:40.426876887 +0700 ++++ relay/config.example.external-kafka.yml 2025-05-15 08:34:21.113311217 +0700 +@@ -7,8 +7,15 @@ + processing: + enabled: true + kafka_config: +- - {name: "bootstrap.servers", value: "kafka:9092"} ++ - {name: "bootstrap.servers", value: "kafka-node1:9092,kafka-node2:9092,kafka-node3:9092"} + - {name: "message.max.bytes", value: 50000000} # 50MB ++ - {name: "security.protocol", value: "PLAINTEXT"} ++ - {name: "sasl.mechanism", value: "PLAIN"} # Remove or comment this line if SASL is not used. ++ - {name: "sasl.username", value: "username"} # Remove or comment this line if SASL is not used. ++ - {name: "sasl.password", value: "password"} # Remove or comment this line if SASL is not used. ++ - {name: "ssl.ca.location", value: "/kafka-certificates/ca.pem"} # Remove or comment this line if SSL is not used. ++ - {name: "ssl.certificate.location", value: "/kafka-certificates/client.pem"} # Remove or comment this line if SSL is not used. ++ - {name: "ssl.key.location", value: "/kafka-certificates/client.key"} # Remove or comment this line if SSL is not used. + redis: redis://redis:6379 + geoip_path: "/geoip/GeoLite2-City.mmdb" + diff --git a/optional-modifications/patches/external-kafka/docker-compose.yml.patch b/optional-modifications/patches/external-kafka/docker-compose.yml.patch new file mode 100644 index 00000000000..ad0328410b7 --- /dev/null +++ b/optional-modifications/patches/external-kafka/docker-compose.yml.patch @@ -0,0 +1,142 @@ +--- docker-compose.yml 2025-03-17 13:32:15.120328412 +0700 ++++ docker-compose.external-kafka.yml 2025-05-15 08:39:05.509951068 +0700 +@@ -26,8 +26,6 @@ + depends_on: + redis: + <<: *depends_on-healthy +- kafka: +- <<: *depends_on-healthy + postgres: + <<: *depends_on-healthy + memcached: +@@ -59,6 +57,14 @@ + SENTRY_EVENT_RETENTION_DAYS: + SENTRY_MAIL_HOST: + SENTRY_MAX_EXTERNAL_SOURCEMAP_SIZE: ++ KAFKA_BOOTSTRAP_SERVERS: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ KAFKA_SECURITY_PROTOCOL: ${KAFKA_SECURITY_PROTOCOL:-PLAINTEXT} ++ KAFKA_SSL_CA_LOCATION: ${KAFKA_SSL_CA_LOCATION:-} ++ KAFKA_SSL_CERTIFICATE_LOCATION: ${KAFKA_SSL_CERTIFICATE_LOCATION:-} ++ KAFKA_SSL_KEY_LOCATION: ${KAFKA_SSL_KEY_LOCATION:-} ++ KAFKA_SASL_MECHANISM: ${KAFKA_SASL_MECHANISM:-} ++ KAFKA_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-} ++ KAFKA_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-} + volumes: + - "sentry-data:/data" + - "./sentry:/etc/sentry" +@@ -69,15 +75,20 @@ + depends_on: + clickhouse: + <<: *depends_on-healthy +- kafka: +- <<: *depends_on-healthy + redis: + <<: *depends_on-healthy + image: "$SNUBA_IMAGE" + environment: + SNUBA_SETTINGS: self_hosted + CLICKHOUSE_HOST: clickhouse +- DEFAULT_BROKERS: "kafka:9092" ++ DEFAULT_BROKERS: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ KAFKA_SECURITY_PROTOCOL: ${KAFKA_SECURITY_PROTOCOL:-PLAINTEXT} ++ KAFKA_SSL_CA_PATH: ${KAFKA_SSL_CA_LOCATION:-} ++ KAFKA_SSL_CERT_PATH: ${KAFKA_SSL_CERTIFICATE_LOCATION:-} ++ KAFKA_SSL_KEY_PATH: ${KAFKA_SSL_KEY_LOCATION:-} ++ KAFKA_SASL_MECHANISM: ${KAFKA_SASL_MECHANISM:-} ++ KAFKA_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-} ++ KAFKA_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-} + REDIS_HOST: redis + UWSGI_MAX_REQUESTS: "10000" + UWSGI_DISABLE_LOGGING: "true" +@@ -140,43 +151,7 @@ + POSTGRES_HOST_AUTH_METHOD: "trust" + volumes: + - "sentry-postgres:/var/lib/postgresql/data" +- kafka: +- <<: *restart_policy +- image: "confluentinc/cp-kafka:7.6.1" +- environment: +- # https://docs.confluent.io/platform/current/installation/docker/config-reference.html#cp-kakfa-example +- KAFKA_PROCESS_ROLES: "broker,controller" +- KAFKA_CONTROLLER_QUORUM_VOTERS: "1001@127.0.0.1:29093" +- KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER" +- KAFKA_NODE_ID: "1001" +- CLUSTER_ID: "MkU3OEVBNTcwNTJENDM2Qk" +- KAFKA_LISTENERS: "PLAINTEXT://0.0.0.0:29092,INTERNAL://0.0.0.0:9093,EXTERNAL://0.0.0.0:9092,CONTROLLER://0.0.0.0:29093" +- KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://127.0.0.1:29092,INTERNAL://kafka:9093,EXTERNAL://kafka:9092" +- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "PLAINTEXT:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT" +- KAFKA_INTER_BROKER_LISTENER_NAME: "PLAINTEXT" +- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: "1" +- KAFKA_OFFSETS_TOPIC_NUM_PARTITIONS: "1" +- KAFKA_LOG_RETENTION_HOURS: "24" +- KAFKA_MESSAGE_MAX_BYTES: "50000000" #50MB or bust +- KAFKA_MAX_REQUEST_SIZE: "50000000" #50MB on requests apparently too +- CONFLUENT_SUPPORT_METRICS_ENABLE: "false" +- KAFKA_LOG4J_LOGGERS: "kafka.cluster=WARN,kafka.controller=WARN,kafka.coordinator=WARN,kafka.log=WARN,kafka.server=WARN,state.change.logger=WARN" +- KAFKA_LOG4J_ROOT_LOGLEVEL: "WARN" +- KAFKA_TOOLS_LOG4J_LOGLEVEL: "WARN" +- ulimits: +- nofile: +- soft: 4096 +- hard: 4096 +- volumes: +- - "sentry-kafka:/var/lib/kafka/data" +- - "sentry-kafka-log:/var/lib/kafka/log" +- - "sentry-secrets:/etc/kafka/secrets" +- healthcheck: +- <<: *healthcheck_defaults +- test: ["CMD-SHELL", "nc -z localhost 9092"] +- interval: 10s +- timeout: 10s +- retries: 30 ++ kafka: !reset null + clickhouse: + <<: *restart_policy + image: clickhouse-self-hosted-local +@@ -475,9 +450,8 @@ + read_only: true + source: ./geoip + target: /geoip ++ - ./certificates/kafka:/kafka-certificates:ro + depends_on: +- kafka: +- <<: *depends_on-healthy + redis: + <<: *depends_on-healthy + web: +@@ -486,15 +460,21 @@ + <<: *restart_policy + image: "$VROOM_IMAGE" + environment: +- SENTRY_KAFKA_BROKERS_PROFILING: "kafka:9092" +- SENTRY_KAFKA_BROKERS_OCCURRENCES: "kafka:9092" ++ SENTRY_KAFKA_BROKERS_PROFILING: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ SENTRY_KAFKA_BROKERS_OCCURRENCES: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ SENTRY_KAFKA_BROKERS_SPANS: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ SENTRY_KAFKA_SECURITY_PROTOCOL: ${KAFKA_SECURITY_PROTOCOL:-PLAINTEXT} ++ SENTRY_KAFKA_SSL_CA_PATH: ${KAFKA_SSL_CA_LOCATION:-} ++ SENTRY_KAFKA_SSL_CERT_PATH: ${KAFKA_SSL_CERTIFICATE_LOCATION:-} ++ SENTRY_KAFKA_SSL_KEY_PATH: ${KAFKA_SSL_KEY_LOCATION:-} ++ SENTRY_KAFKA_SASL_MECHANISM: ${KAFKA_SASL_MECHANISM:-} ++ SENTRY_KAFKA_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-} ++ SENTRY_KAFKA_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-} + SENTRY_BUCKET_PROFILES: file://localhost//var/lib/sentry-profiles + SENTRY_SNUBA_HOST: "http://snuba-api:1218/" + volumes: + - sentry-vroom:/var/lib/sentry-profiles +- depends_on: +- kafka: +- <<: *depends_on-healthy ++ - ./certificates/kafka:/kafka-certificates:ro + profiles: + - feature-complete + vroom-cleanup: +@@ -523,8 +503,6 @@ + external: true + sentry-redis: + external: true +- sentry-kafka: +- external: true + sentry-clickhouse: + external: true + sentry-symbolicator: diff --git a/optional-modifications/patches/external-kafka/sentry.conf.example.py.patch b/optional-modifications/patches/external-kafka/sentry.conf.example.py.patch new file mode 100644 index 00000000000..abc755c00a0 --- /dev/null +++ b/optional-modifications/patches/external-kafka/sentry.conf.example.py.patch @@ -0,0 +1,21 @@ +--- sentry/sentry.conf.example.py 2025-05-15 08:27:40.427876868 +0700 ++++ sentry/sentry.conf.example.external-kafka.py 2025-05-15 08:32:44.845127931 +0700 +@@ -132,9 +132,17 @@ + SENTRY_CACHE = "sentry.cache.redis.RedisCache" + + DEFAULT_KAFKA_OPTIONS = { +- "bootstrap.servers": "kafka:9092", ++ "bootstrap.servers": env("KAFKA_BOOTSTRAP_SERVERS", "kafka:9092"), + "message.max.bytes": 50000000, + "socket.timeout.ms": 1000, ++ "security.protocol": env("KAFKA_SECURITY_PROTOCOL", "PLAINTEXT"), # Valid options are PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL ++ # If you don't use any of these options below, you can remove them or set them to `None`. ++ "sasl.mechanism": env("KAFKA_SASL_MECHANISM", None), # Valid options are PLAIN, SCRAM-SHA-256, SCRAM-SHA-512. Other mechanism might be unavailable. ++ "sasl.username": env("KAFKA_SASL_USERNAME", None), ++ "sasl.password": env("KAFKA_SASL_PASSWORD", None), ++ "ssl.ca.location": env("KAFKA_SSL_CA_LOCATION", None), # Remove this line if SSL is not used. ++ "ssl.certificate.location": env("KAFKA_SSL_CERTIFICATE_LOCATION", None), # Remove this line if SSL is not used. ++ "ssl.key.location": env("KAFKA_SSL_KEY_LOCATION", None), # Remove this line if SSL is not used. + } + + SENTRY_EVENTSTREAM = "sentry.eventstream.kafka.KafkaEventStream" From 2b6bd5f9e86cce1ef36b45ab23a4eaf0b1ccd119 Mon Sep 17 00:00:00 2001 From: Vita Chumakova Date: Sat, 14 Jun 2025 03:49:46 +0400 Subject: [PATCH 196/287] feat: migrate to arm64-compatible smtp image (#3746) --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b1805dd3a09..600e757cd12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -91,12 +91,12 @@ x-snuba-defaults: &snuba_defaults services: smtp: <<: *restart_policy - platform: linux/amd64 - image: tianon/exim4 - hostname: "${SENTRY_MAIL_HOST:-}" + image: registry.gitlab.com/egos-tech/smtp volumes: - "sentry-smtp:/var/spool/exim4" - "sentry-smtp-log:/var/log/exim4" + environment: + - MAILNAME=${SENTRY_MAIL_HOST:-} memcached: <<: *restart_policy image: "memcached:1.6.26-alpine" From 66c057b4e2281ec009811023fad33ebcc9131be0 Mon Sep 17 00:00:00 2001 From: Nikita Korolev <66738864+doc-sheet@users.noreply.github.com> Date: Sat, 14 Jun 2025 02:50:26 +0300 Subject: [PATCH 197/287] enable shell linter for more scripts (#3748) --- .github/workflows/shellcheck.yml | 8 +++++-- scripts/_lib.sh | 32 +++++++++++++++---------- scripts/bump-version.sh | 1 + scripts/restore.sh | 1 + unit-test.sh | 2 +- workstation/200_download-self-hosted.sh | 2 +- 6 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index e2d06d1ae53..c5cc25e8cbb 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -23,12 +23,16 @@ jobs: - name: Run ShellCheck run: | git diff --name-only -z `git merge-base origin/master HEAD` -- \ - 'install/_lib.sh' \ + install/_lib.sh \ + 'optional-modifications/**.sh' \ + 'scripts/**.sh' \ + unit-test.sh \ + 'workstation/**.sh' \ | xargs -0 -r -- \ shellcheck \ --shell=bash \ - --exclude=SC1090,SC1091 \ --format=json1 \ + --external-sources \ | jq -r ' .comments | map(.level |= if ([.] | inside(["info", "style"])) then "notice" else . end) diff --git a/scripts/_lib.sh b/scripts/_lib.sh index a742f8acc51..ba57fc42a33 100755 --- a/scripts/_lib.sh +++ b/scripts/_lib.sh @@ -7,7 +7,7 @@ if [ -n "${DEBUG:-}" ]; then fi function confirm() { - read -p "$1 [y/n] " confirmation + read -r -p "$1 [y/n] " confirmation if [ "$confirmation" != "y" ]; then echo "Canceled. 😅" exit @@ -26,8 +26,7 @@ function reset() { # we're targeting a valid tag here. Do this early in order to fail fast. if [ -n "$version" ]; then set +e - git rev-parse --verify --quiet "refs/tags/$version" >/dev/null - if [ $? -gt 0 ]; then + if ! git rev-parse --verify --quiet "refs/tags/$version" >/dev/null; then echo "Bad version: $version" exit fi @@ -43,12 +42,15 @@ function reset() { echo "Okay ... good luck! 😰" fi + # assert that commands are defined + : "${dc:?}" "${cmd:?}" + # Hit the reset button. $dc down --volumes --remove-orphans --rmi local # Remove any remaining (likely external) volumes with name matching 'sentry-.*'. for volume in $(docker volume list --format '{{ .Name }}' | grep '^sentry-'); do - docker volume remove $volume >/dev/null && + docker volume remove "$volume" >/dev/null && echo "Removed volume: $volume" || echo "Skipped volume: $volume" done @@ -60,30 +62,34 @@ function reset() { } function backup() { + local type + type=${1:-"global"} - touch $(pwd)/sentry/backup.json - chmod 666 $(pwd)/sentry/backup.json - $dc run -v $(pwd)/sentry:/sentry-data/backup --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web export $type /sentry-data/backup/backup.json + touch "${PWD}/sentry/backup.json" + chmod 666 "${PWD}/sentry/backup.json" + $dc run -v "${PWD}/sentry:/sentry-data/backup" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web export "$type" /sentry-data/backup/backup.json } function restore() { - type=${1:-"global"} - $dc run --rm -T web import $type /etc/sentry/backup.json + local type + + type="${1:-global}" + $dc run --rm -T web import "$type" /etc/sentry/backup.json } # Needed variables to source error-handling script MINIMIZE_DOWNTIME="${MINIMIZE_DOWNTIME:-}" -STOP_TIMEOUT=60 +export STOP_TIMEOUT=60 # Save logs in order to send envelope to Sentry -log_file=sentry_"${cmd%% *}"_log-$(date +'%Y-%m-%d_%H-%M-%S').txt +log_file="sentry_${cmd%% *}_log-$(date +%Y-%m-%d_%H-%M-%S).txt" exec &> >(tee -a "$log_file") version="" while (($#)); do case "$1" in - --report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=1 ;; - --no-report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=0 ;; + --report-self-hosted-issues) export REPORT_SELF_HOSTED_ISSUES=1 ;; + --no-report-self-hosted-issues) export REPORT_SELF_HOSTED_ISSUES=0 ;; *) version=$1 ;; esac shift diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 5e53ec81b20..f5853a3e2f4 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -7,4 +7,5 @@ NEW_VERSION="$2" sed -i -e "s/^\(SENTRY\|SNUBA\|RELAY\|SYMBOLICATOR\|TASKBROKER\|VROOM\)_IMAGE=\([^:]\+\):.\+\$/\1_IMAGE=\2:$NEW_VERSION/" .env sed -i -e "s/^\# Self-Hosted Sentry .*/# Self-Hosted Sentry $NEW_VERSION/" README.md +[ -z "$OLD_VERSION" ] || echo "Previous version: $OLD_VERSION" echo "New version: $NEW_VERSION" diff --git a/scripts/restore.sh b/scripts/restore.sh index ededd3114f3..ae3666b0ec6 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash cmd="restore $1" source scripts/_lib.sh + $cmd diff --git a/unit-test.sh b/unit-test.sh index 01a945e3777..8dbc0d727bd 100755 --- a/unit-test.sh +++ b/unit-test.sh @@ -5,7 +5,7 @@ export REPORT_SELF_HOSTED_ISSUES=0 # will be over-ridden in the relevant test FORCE_CLEAN=1 "./scripts/reset.sh" fail=0 for test_file in _unit-test/*-test.sh; do - if [ "$1" -a "$1" != "$test_file" ]; then + if [ -n "$1" ] && [ "$1" != "$test_file" ]; then echo "🙊 Skipping $test_file ..." continue fi diff --git a/workstation/200_download-self-hosted.sh b/workstation/200_download-self-hosted.sh index a07735d7003..406a52c184d 100644 --- a/workstation/200_download-self-hosted.sh +++ b/workstation/200_download-self-hosted.sh @@ -1,5 +1,5 @@ #!/bin/bash -# +set -eo pipefail # Create getsentry folder and enter. mkdir /home/user/getsentry From 45c21c42bbbc3067aa3d5edeb2f0d0e747fa0f9d Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Sun, 15 Jun 2025 18:06:17 +0000 Subject: [PATCH 198/287] release: 25.6.0 --- .env | 12 ++++++------ CHANGELOG.md | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.env b/.env index a73d4dd4ba7..2a80e2b330e 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -TASKBROKER_IMAGE=getsentry/taskbroker:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.6.0 +SNUBA_IMAGE=getsentry/snuba:25.6.0 +RELAY_IMAGE=getsentry/relay:25.6.0 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.6.0 +TASKBROKER_IMAGE=getsentry/taskbroker:25.6.0 +VROOM_IMAGE=getsentry/vroom:25.6.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index d421410f31c..1460ee3a644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 25.6.0 + +### Various fixes & improvements + +- enable shell linter for more scripts (#3748) by @doc-sheet +- feat: migrate to arm64-compatible smtp image (#3746) by @ezhevita +- Introduce patches with external kafka (#3521) by @aldy505 +- add shellcheck action to lint bash scripts (#3710) by @doc-sheet +- tests: Install version 2.x of Python SDK (#3745) by @sentrivana +- feat(features): enable continuous profiling (#3742) by @aldy505 +- feat: Add taskbroker + worker + scheduler (#3738) by @markstory +- fix(profiles): Run the profile chunks consumer (#3739) by @phacops +- chore: prune removed feature flags on main repository (#3731) by @aldy505 +- remove index workaround (#3730) by @asottile-sentry +- Make usage of Python SDK future proof (#3714) by @antonpirker + ## 25.5.1 ### Various fixes & improvements From e07445d6be41793165316a3e077ebec343740530 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 17 Jun 2025 14:00:04 +0100 Subject: [PATCH 199/287] fix(vroom): Explicitly set PROFILES_DIR for upcoming change (#3759) PROFILES_DIR was defaulting to `/var/lib/sentry-profiles` which requires root access. When Vroom image decided to go with non-root default user, this started causing permission issues. Now the image is being refactored and it will not use `/var/lib/sentry-profiles` as the default path so we need to explicitly pass it. --- .env | 2 +- docker-compose.yml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.env b/.env index a73d4dd4ba7..fbb036f177a 100644 --- a/.env +++ b/.env @@ -14,7 +14,7 @@ SNUBA_IMAGE=getsentry/snuba:nightly RELAY_IMAGE=getsentry/relay:nightly SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly TASKBROKER_IMAGE=getsentry/taskbroker:nightly -VROOM_IMAGE=getsentry/vroom:nightly +VROOM_IMAGE=getsentry/vroom:a8e9e04 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/docker-compose.yml b/docker-compose.yml index 600e757cd12..e4e3b77fee7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -511,7 +511,7 @@ services: environment: SENTRY_KAFKA_BROKERS_PROFILING: "kafka:9092" SENTRY_KAFKA_BROKERS_OCCURRENCES: "kafka:9092" - SENTRY_BUCKET_PROFILES: file://localhost//var/lib/sentry-profiles + PROFILES_DIR: "/var/lib/sentry-profiles" SENTRY_SNUBA_HOST: "http://snuba-api:1218/" volumes: - sentry-vroom:/var/lib/sentry-profiles @@ -529,10 +529,11 @@ services: BASE_IMAGE: "$VROOM_IMAGE" entrypoint: "/entrypoint.sh" environment: + PROFILES_DIR: "/var/lib/sentry-profiles" # Leaving the value empty to just pass whatever is set # on the host system (or in the .env file) SENTRY_EVENT_RETENTION_DAYS: - command: '"0 0 * * * find /var/lib/sentry-profiles -type f -mtime +$SENTRY_EVENT_RETENTION_DAYS -delete"' + command: '"0 0 * * * find $PROFILES_DIR -type f -mtime +$SENTRY_EVENT_RETENTION_DAYS -delete"' volumes: - sentry-vroom:/var/lib/sentry-profiles profiles: From 019d372df265e7e2cbf32d31d37850203e64bf93 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Tue, 17 Jun 2025 13:26:01 -0700 Subject: [PATCH 200/287] Revert "fix(vroom): Explicitly set PROFILES_DIR for upcoming change" (#3760) * Revert "fix(vroom): Explicitly set PROFILES_DIR for upcoming change (#3759)" This reverts commit e07445d6be41793165316a3e077ebec343740530. It also very importantly changes where we mount the profiles volume which fixes the issue. Our theory is as follows: 1. Vroom Dockerfile had a line doing `mkdirp /var/lib/sentry-profiles` at image build time. This makes the directory owned by `root` 2. When we mount over that directory, and change permissions we can store the permissions changes _in_ the directory but not the directory itself 3. So when we start the vroom image with the new mount, the contents are owned by `vroom` but the main directory is still owned by `root`. This is also why [this approach](https://github.com/getsentry/vroom/pull/601/files/a23a4e395269ca39fd9bd93ecf902cb42530b5cd) worked as the entrypoint script did this at the start of every container instance. --------- Co-authored-by: Burak Yigit Kaya --- .env | 2 +- docker-compose.yml | 15 +++++---------- install.sh | 1 + .../ensure-correct-permissions-profiles-dir.sh | 7 +++++++ .../external-kafka/docker-compose.yml.patch | 4 ++-- 5 files changed, 16 insertions(+), 13 deletions(-) create mode 100755 install/ensure-correct-permissions-profiles-dir.sh diff --git a/.env b/.env index fbb036f177a..a73d4dd4ba7 100644 --- a/.env +++ b/.env @@ -14,7 +14,7 @@ SNUBA_IMAGE=getsentry/snuba:nightly RELAY_IMAGE=getsentry/relay:nightly SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly TASKBROKER_IMAGE=getsentry/taskbroker:nightly -VROOM_IMAGE=getsentry/vroom:a8e9e04 +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/docker-compose.yml b/docker-compose.yml index e4e3b77fee7..70c9327dfe6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -131,11 +131,7 @@ services: # Using default user "postgres" from sentry/sentry.conf.example.py or value of POSTGRES_USER if provided test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] command: - [ - "postgres", - "-c", - "max_connections=${POSTGRES_MAX_CONNECTIONS:-100}", - ] + ["postgres", "-c", "max_connections=${POSTGRES_MAX_CONNECTIONS:-100}"] environment: POSTGRES_HOST_AUTH_METHOD: "trust" volumes: @@ -511,10 +507,10 @@ services: environment: SENTRY_KAFKA_BROKERS_PROFILING: "kafka:9092" SENTRY_KAFKA_BROKERS_OCCURRENCES: "kafka:9092" - PROFILES_DIR: "/var/lib/sentry-profiles" + SENTRY_BUCKET_PROFILES: file:///var/vroom/sentry-profiles SENTRY_SNUBA_HOST: "http://snuba-api:1218/" volumes: - - sentry-vroom:/var/lib/sentry-profiles + - sentry-vroom:/var/vroom/sentry-profiles depends_on: kafka: <<: *depends_on-healthy @@ -529,13 +525,12 @@ services: BASE_IMAGE: "$VROOM_IMAGE" entrypoint: "/entrypoint.sh" environment: - PROFILES_DIR: "/var/lib/sentry-profiles" # Leaving the value empty to just pass whatever is set # on the host system (or in the .env file) SENTRY_EVENT_RETENTION_DAYS: - command: '"0 0 * * * find $PROFILES_DIR -type f -mtime +$SENTRY_EVENT_RETENTION_DAYS -delete"' + command: '"0 0 * * * find /var/vroom/sentry-profiles -type f -mtime +$SENTRY_EVENT_RETENTION_DAYS -delete"' volumes: - - sentry-vroom:/var/lib/sentry-profiles + - sentry-vroom:/var/vroom/sentry-profiles profiles: - feature-complete diff --git a/install.sh b/install.sh index 86e4de1b404..d7f8a036f4e 100755 --- a/install.sh +++ b/install.sh @@ -38,6 +38,7 @@ source install/update-docker-images.sh source install/build-docker-images.sh source install/bootstrap-snuba.sh source install/upgrade-postgres.sh +source install/ensure-correct-permissions-profiles-dir.sh source install/set-up-and-migrate-database.sh source install/geoip.sh source install/setup-js-sdk-assets.sh diff --git a/install/ensure-correct-permissions-profiles-dir.sh b/install/ensure-correct-permissions-profiles-dir.sh new file mode 100755 index 00000000000..68b782bc8a5 --- /dev/null +++ b/install/ensure-correct-permissions-profiles-dir.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# TODO: Remove this after the next hard-stop + +echo "${_group}Ensuring correct permissions on profiles directory ..." +$dcr --no-deps --entrypoint /bin/bash --user root vroom -c 'chown -R vroom:vroom /var/vroom/sentry-profiles && chmod -R o+rwx /var/vroom/sentry-profiles' +echo "${_endgroup}" diff --git a/optional-modifications/patches/external-kafka/docker-compose.yml.patch b/optional-modifications/patches/external-kafka/docker-compose.yml.patch index ad0328410b7..cd669acc784 100644 --- a/optional-modifications/patches/external-kafka/docker-compose.yml.patch +++ b/optional-modifications/patches/external-kafka/docker-compose.yml.patch @@ -120,10 +120,10 @@ + SENTRY_KAFKA_SASL_MECHANISM: ${KAFKA_SASL_MECHANISM:-} + SENTRY_KAFKA_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-} + SENTRY_KAFKA_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-} - SENTRY_BUCKET_PROFILES: file://localhost//var/lib/sentry-profiles + SENTRY_BUCKET_PROFILES: file:///var/vroom/sentry-profiles SENTRY_SNUBA_HOST: "http://snuba-api:1218/" volumes: - - sentry-vroom:/var/lib/sentry-profiles + - sentry-vroom:/var/vroom/sentry-profiles - depends_on: - kafka: - <<: *depends_on-healthy From 433eed8fb7f80a1dceeebb58691f49cba462e1a0 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 17 Jun 2025 22:47:23 +0100 Subject: [PATCH 201/287] ref(js-assets): Simplify how we call nginx container (#3761) --- _unit-test/js-sdk-assets-test.sh | 6 +++--- install/setup-js-sdk-assets.sh | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/_unit-test/js-sdk-assets-test.sh b/_unit-test/js-sdk-assets-test.sh index 7e2306ecb97..c46c7a51e5f 100755 --- a/_unit-test/js-sdk-assets-test.sh +++ b/_unit-test/js-sdk-assets-test.sh @@ -9,9 +9,9 @@ export SETUP_JS_SDK_ASSETS=1 source install/setup-js-sdk-assets.sh -sdk_files=$($dcr --no-deps -v "sentry-nginx-www:/var/www" nginx ls -lah /var/www/js-sdk/) -sdk_tree=$($dcr --no-deps -v "sentry-nginx-www:/var/www" nginx tree /var/www/js-sdk/ | tail -n 1) -non_empty_file_count=$($dcr --no-deps -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk/ -type f -size +1k | wc -l) +sdk_files=$($dcr --no-deps nginx ls -lah /var/www/js-sdk/) +sdk_tree=$($dcr --no-deps nginx tree /var/www/js-sdk/ | tail -n 1) +non_empty_file_count=$($dcr --no-deps nginx find /var/www/js-sdk/ -type f -size +1k | wc -l) # `sdk_files` should contains 5 lines, '4.*', '5.*', '6.*', `7.*` and `8.*` echo $sdk_files diff --git a/install/setup-js-sdk-assets.sh b/install/setup-js-sdk-assets.sh index 51c5e408191..358b67a951a 100644 --- a/install/setup-js-sdk-assets.sh +++ b/install/setup-js-sdk-assets.sh @@ -9,7 +9,7 @@ if [[ "${SETUP_JS_SDK_ASSETS:-}" == "1" ]]; then # `SETUP_JS_SDK_KEEP_OLD_ASSETS` to any value. if [[ -z "${SETUP_JS_SDK_KEEP_OLD_ASSETS:-}" ]]; then echo "Cleaning up old JS SDK assets..." - $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx rm -rf /var/www/js-sdk/* + $dcr --no-deps nginx rm -rf /var/www/js-sdk/* fi $dbuild -t sentry-self-hosted-jq-local --platform="$DOCKER_PLATFORM" jq @@ -35,12 +35,12 @@ if [[ "${SETUP_JS_SDK_ASSETS:-}" == "1" ]]; then variants="{bundle,bundle.tracing,bundle.tracing.replay,bundle.replay,bundle.tracing.replay.feedback,bundle.feedback}" # Download those versions & variants using curl - $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx curl -w '%{response_code} %{url}\n' --no-progress-meter --compressed --retry 3 --create-dirs -fLo "/var/www/js-sdk/#1/#2.min.js" "https://browser.sentry-cdn.com/$%7Bversions%7D/$%7Bvariants%7D.min.js" || true + $dcr --no-deps nginx curl -w '%{response_code} %{url}\n' --no-progress-meter --compressed --retry 3 --create-dirs -fLo "/var/www/js-sdk/#1/#2.min.js" "https://browser.sentry-cdn.com/$%7Bversions%7D/$%7Bvariants%7D.min.js" || true # Make sure permissions are correct # See https://github.com/getsentry/self-hosted/issues/3614 for reported issue - $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk -type d -exec chmod 755 {} \; - $dcr --no-deps --rm -v "sentry-nginx-www:/var/www" nginx find /var/www/js-sdk -type f -exec chmod 644 {} \; + $dcr --no-deps nginx find /var/www/js-sdk -type d -exec chmod 755 {} \; + $dcr --no-deps nginx find /var/www/js-sdk -type f -exec chmod 644 {} \; echo "${_endgroup}" fi From 4123963817ee545cd7c4c03e578a1ef28136520b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 18 Jun 2025 19:40:07 +0000 Subject: [PATCH 202/287] build: Set master version to nightly #skip-changelog --- .env | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 2a80e2b330e..a73d4dd4ba7 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.6.0 -SNUBA_IMAGE=getsentry/snuba:25.6.0 -RELAY_IMAGE=getsentry/relay:25.6.0 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.6.0 -TASKBROKER_IMAGE=getsentry/taskbroker:25.6.0 -VROOM_IMAGE=getsentry/vroom:25.6.0 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +TASKBROKER_IMAGE=getsentry/taskbroker:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 43d7d967212f8a910af3c389ac7f47e3aae5c93b Mon Sep 17 00:00:00 2001 From: yildizozgur Date: Thu, 19 Jun 2025 03:07:12 +0200 Subject: [PATCH 203/287] feat: enable customization sentry DSN endpoint (#3747) feat: enable customization sentry DSN endpoint Update sentry/sentry.conf.example.py --- sentry/sentry.conf.example.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 79c7aefa6cd..ed30ad0a67f 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -341,6 +341,14 @@ def get_internal_network(): # https://django-csp.readthedocs.io/en/latest/configuration.html # CSP_SCRIPT_SRC += ["example.com"] +############################ +# Sentry Endpoint Settings # +############################ + +# URI Prefixes for generating DSN URLs +# (default is URL_PREFIX) +# SENTRY_ENDPOINT = "https://sentry.ingest.example.com/" + ################# # CSRF Settings # ################# From d80a7d9c86cd290c36b4d64a26fbb438a408c6ad Mon Sep 17 00:00:00 2001 From: Mark Story Date: Thu, 19 Jun 2025 09:56:46 -0400 Subject: [PATCH 204/287] fix(taskworker) Remove num-brokers (#3769) The num-brokers option generates broker host names that don't exist in self-hosted. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 70c9327dfe6..65bee636869 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -500,7 +500,7 @@ services: command: run taskworker-scheduler taskworker: <<: *sentry_defaults - command: run taskworker --concurrency=4 --rpc-host=taskbroker:50051 --num-brokers=1 + command: run taskworker --concurrency=4 --rpc-host=taskbroker:50051 vroom: <<: *restart_policy image: "$VROOM_IMAGE" From c794a189d04870d37e9872d0353531b89b94973d Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 20 Jun 2025 22:34:01 +0000 Subject: [PATCH 205/287] release: 25.6.1 --- .env | 12 ++++++------ CHANGELOG.md | 10 ++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.env b/.env index a73d4dd4ba7..3a52ecfc705 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -TASKBROKER_IMAGE=getsentry/taskbroker:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.6.1 +SNUBA_IMAGE=getsentry/snuba:25.6.1 +RELAY_IMAGE=getsentry/relay:25.6.1 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.6.1 +TASKBROKER_IMAGE=getsentry/taskbroker:25.6.1 +VROOM_IMAGE=getsentry/vroom:25.6.1 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1460ee3a644..d5db9bd0166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 25.6.1 + +### Various fixes & improvements + +- fix(taskworker) Remove num-brokers (#3769) by @markstory +- feat: enable customization sentry DSN endpoint (#3747) by @yildizozgur +- ref(js-assets): Simplify how we call nginx container (#3761) by @BYK +- Revert "fix(vroom): Explicitly set PROFILES_DIR for upcoming change" (#3760) by @hubertdeng123 +- fix(vroom): Explicitly set PROFILES_DIR for upcoming change (#3759) by @BYK + ## 25.6.0 ### Various fixes & improvements From 494051b8df0c6e18badb701daf85b3818c170888 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 20 Jun 2025 22:45:39 +0000 Subject: [PATCH 206/287] build: Set master version to nightly #skip-changelog --- .env | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 3a52ecfc705..a73d4dd4ba7 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.6.1 -SNUBA_IMAGE=getsentry/snuba:25.6.1 -RELAY_IMAGE=getsentry/relay:25.6.1 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.6.1 -TASKBROKER_IMAGE=getsentry/taskbroker:25.6.1 -VROOM_IMAGE=getsentry/vroom:25.6.1 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +TASKBROKER_IMAGE=getsentry/taskbroker:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 0fce96cc112e25800cda22d7941794c88f2cba38 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 26 Jun 2025 05:07:43 +0700 Subject: [PATCH 207/287] chore: provide detailed note for sentry endpoint settings (#3780) Follow up for https://github.com/getsentry/self-hosted/pull/3747 --- sentry/sentry.conf.example.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index ed30ad0a67f..85f55906f76 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -345,8 +345,12 @@ def get_internal_network(): # Sentry Endpoint Settings # ############################ -# URI Prefixes for generating DSN URLs -# (default is URL_PREFIX) +# If your Sentry installation has different hostnames for ingestion and web UI, +# in which your web UI is accessible via private corporate network, yet your +# ingestion hostname is accessible from the public internet, you can uncomment +# this following options in order to have the ingestion hostname rendered +# correctly on the SDK configuration UI. +# # SENTRY_ENDPOINT = "https://sentry.ingest.example.com/" ################# From bf660f3302e6e96d7e0dc47b91e42ad3fc1e2a56 Mon Sep 17 00:00:00 2001 From: Tobias Wilfert <36408720+tobias-wilfert@users.noreply.github.com> Date: Fri, 27 Jun 2025 01:12:13 +0200 Subject: [PATCH 208/287] fix: Increase timeout for flakey test (#3781) --- _integration-test/test_01_basics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index 741d4fd807c..14ffa253067 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -22,7 +22,7 @@ SENTRY_TEST_HOST = os.getenv("SENTRY_TEST_HOST", "http://localhost:9000/") TEST_USER = "test@example.com" TEST_PASS = "test123TEST" -TIMEOUT_SECONDS = 60 +TIMEOUT_SECONDS = 120 def poll_for_response( From fa34d4922a774c8489019fccc5fca49dc1397002 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 30 Jun 2025 22:18:12 +0000 Subject: [PATCH 209/287] release: 25.6.2 --- .env | 12 ++++++------ CHANGELOG.md | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.env b/.env index a73d4dd4ba7..ff49588ae70 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -TASKBROKER_IMAGE=getsentry/taskbroker:nightly -VROOM_IMAGE=getsentry/vroom:nightly +SENTRY_IMAGE=getsentry/sentry:25.6.2 +SNUBA_IMAGE=getsentry/snuba:25.6.2 +RELAY_IMAGE=getsentry/relay:25.6.2 +SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.6.2 +TASKBROKER_IMAGE=getsentry/taskbroker:25.6.2 +VROOM_IMAGE=getsentry/vroom:25.6.2 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index d5db9bd0166..3f90ca37103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 25.6.2 + +### Various fixes & improvements + +- fix: Increase timeout for flakey test (#3781) by @tobias-wilfert +- chore: provide detailed note for sentry endpoint settings (#3780) by @aldy505 + ## 25.6.1 ### Various fixes & improvements From acbdee40df6d4ecf9eb4645ac5b45ec739f537cf Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 30 Jun 2025 22:47:22 +0000 Subject: [PATCH 210/287] build: Set master version to nightly #skip-changelog --- .env | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index ff49588ae70..a73d4dd4ba7 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:25.6.2 -SNUBA_IMAGE=getsentry/snuba:25.6.2 -RELAY_IMAGE=getsentry/relay:25.6.2 -SYMBOLICATOR_IMAGE=getsentry/symbolicator:25.6.2 -TASKBROKER_IMAGE=getsentry/taskbroker:25.6.2 -VROOM_IMAGE=getsentry/vroom:25.6.2 +SENTRY_IMAGE=getsentry/sentry:nightly +SNUBA_IMAGE=getsentry/snuba:nightly +RELAY_IMAGE=getsentry/relay:nightly +SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly +TASKBROKER_IMAGE=getsentry/taskbroker:nightly +VROOM_IMAGE=getsentry/vroom:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From b5a0158871daf6b0e1c7dd5a3b8437bc2c118735 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 2 Jul 2025 15:30:20 +0700 Subject: [PATCH 211/287] ci: run tests on arm64 (#3750) * ci: run tests on arm64 * ci: runner name should be arm, not arm64 * ci: retain old job name to not mess with CI protection rules * ci: integration test should not use plural form --- .github/workflows/test.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 672a2802e78..e02e81eb1f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,8 +21,11 @@ defaults: jobs: unit-test: if: github.repository_owner == 'getsentry' - runs-on: ubuntu-22.04 - name: "unit tests" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm] + name: ${{ matrix.os == 'ubuntu-24.04-arm' && 'unit tests (arm64)' || 'unit tests' }} steps: - name: Checkout uses: actions/checkout@v4 @@ -35,8 +38,11 @@ jobs: integration-test: if: github.repository_owner == 'getsentry' - runs-on: ubuntu-22.04 - name: integration test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm] + name: ${{ matrix.os == 'ubuntu-24.04-arm' && 'integration test (arm64)' || 'integration test' }} env: REPORT_SELF_HOSTED_ISSUES: 0 SELF_HOSTED_TESTING_DSN: ${{ vars.SELF_HOSTED_TESTING_DSN }} From d3a068df8407825af694c5e4b416ef392aa817f2 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 2 Jul 2025 16:12:47 +0700 Subject: [PATCH 212/287] feat: make `system.secret-key` configurable from environment variables (#3783) --- sentry/config.example.yml | 3 +++ sentry/sentry.conf.example.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/sentry/config.example.yml b/sentry/config.example.yml index 54fe0c9c813..3c499e7a625 100644 --- a/sentry/config.example.yml +++ b/sentry/config.example.yml @@ -52,6 +52,9 @@ system.internal-url-prefix: 'http://web:9000/' # If this file ever becomes compromised, it's important to generate a new key. # Changing this value will result in all current sessions being invalidated. # A new key can be generated with `$ sentry config generate-secret-key` +# +# If you are using SENTRY_SYSTEM_SECRET_KEY that is being set on your `.env` or `.env.custom` file, +# you should remove this line below as it won't be used anyway. system.secret-key: '!!changeme!!' # The ``redis.clusters`` setting is used, unsurprisingly, to configure Redis diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 85f55906f76..4b69c32e870 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -64,10 +64,25 @@ def get_internal_network(): # and thus various UI optimizations should be enabled. SENTRY_SINGLE_ORGANIZATION = True +# Sentry event retention days specifies how long events are retained in the database. +# This should be set on your `.env` or `.env.custom` file, instead of modifying +# the value here. +# NOTE: The longer the days, the more disk space is required. SENTRY_OPTIONS["system.event-retention-days"] = int( env("SENTRY_EVENT_RETENTION_DAYS", "90") ) +# The secret key is being used for various cryptographic operations, such as +# generating a CSRF token, session token, and registering Relay instances. +# The secret key value should be set on your `.env` or `.env.custom` file +# instead of modifying the value here. +# +# If the key ever becomes compromised, it's important to generate a new key. +# Changing this value will result in all current sessions being invalidated. +# A new key can be generated with `$ sentry config generate-secret-key` +if env("SENTRY_SYSTEM_SECRET_KEY"): + SENTRY_OPTIONS["system.secret-key"] = env("SENTRY_SYSTEM_SECRET_KEY", "") + # Self-hosted Sentry infamously has a lot of Docker containers required to make # all the features work. Oftentimes, users don't use the full feature set that # requires all the containers. This is a way to enable only the error monitoring From 06d0fb1715ae10ea9a9757f4ebc2e6be2cc64a6e Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Mon, 7 Jul 2025 12:23:50 -0400 Subject: [PATCH 213/287] feat(uptime): Enable uptime in self-hosted (#3787) --- .env | 1 + docker-compose.yml | 32 ++++++++++++++++++++++++++++++++ sentry/sentry.conf.example.py | 15 +++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/.env b/.env index a73d4dd4ba7..bb616638144 100644 --- a/.env +++ b/.env @@ -15,6 +15,7 @@ RELAY_IMAGE=getsentry/relay:nightly SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly TASKBROKER_IMAGE=getsentry/taskbroker:nightly VROOM_IMAGE=getsentry/vroom:nightly +UPTIME_CHECKER_IMAGE=getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/docker-compose.yml b/docker-compose.yml index 65bee636869..c75376c5964 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -305,6 +305,11 @@ services: command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset profiles: - feature-complete + snuba-uptime-results-consumer: + <<: *snuba_defaults + command: rust-consumer --storage uptime_monitor_checks --consumer-group snuba-uptime-results --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + profiles: + - feature-complete symbolicator: <<: *restart_policy image: "$SYMBOLICATOR_IMAGE" @@ -415,6 +420,11 @@ services: command: run consumer monitors-clock-tasks --consumer-group monitors-clock-tasks profiles: - feature-complete + uptime-results: + <<: *sentry_defaults + command: run consumer uptime-results --consumer-group uptime-results + profiles: + - feature-complete post-process-forwarder-transactions: <<: *sentry_defaults command: run consumer --no-strict-offset-reset post-process-forwarder-transactions --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-transactions-commit-log --synchronize-commit-group transactions_group @@ -533,6 +543,28 @@ services: - sentry-vroom:/var/vroom/sentry-profiles profiles: - feature-complete + uptime-checker: + <<: *restart_policy + image: "$UPTIME_CHECKER_IMAGE" + command: run + environment: + UPTIME_CHECKER_RESULTS_KAFKA_CLUSTER: kafka:9092 + UPTIME_CHECKER_REDIS_HOST: redis://redis:6379 + # Set to `true` will allow uptime checks against private IP addresses + UPTIME_CHECKER_ALLOW_INTERNAL_IPS: "false" + # The number of times to retry failed checks before reporting them as failed + UPTIME_CHECKER_FAILURE_RETRIES: "1" + # DNS name servers to use when making checks in the http checker. + # Separated by commas. Leaving this unset will default to the systems dns + # resolver. + #UPTIME_CHECKER_HTTP_CHECKER_DNS_NAMESERVERS: "8.8.8.8,8.8.4.4" + depends_on: + kafka: + <<: *depends_on-healthy + redis: + <<: *depends_on-healthy + profiles: + - feature-complete volumes: # These store application data that should persist across restarts. diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 4b69c32e870..c8a4afbbd21 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -329,9 +329,24 @@ def get_internal_network(): "organizations:continuous-profiling", "organizations:continuous-profiling-stats", ) + # Uptime related flags + + ( + "organizations:uptime", + "organizations:uptime-create-issues", + # TODO(epurkhiser): We can remove remove these in 25.8.0 since + # we'll have released this issue group type + # (https://github.com/getsentry/sentry/pull/94827) + "organizations:issue-uptime-domain-failure-visible", + "organizations:issue-uptime-domain-failure-ingest", + "organizations:issue-uptime-domain-failure-post-process-group", + ) } ) +# TODO(epurkhiser): In 25.8.0 we can drop this option override as we've made it +# default in sentry (https://github.com/getsentry/sentry/pull/94822) +SENTRY_OPTIONS["uptime.snuba_uptime_results.enabled"] = True + ####################### # MaxMind Integration # ####################### From e2ad04d564385f05e6dfbc62c7ff1c16d5a81297 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 8 Jul 2025 08:42:50 +0700 Subject: [PATCH 214/287] feat: run EAP-related containers (#3778) * feat: run snuba-items consumer * feat: remove process-spans as it turns out relay does not publish to that topic yet * feat: try re-adding process-spans * feat: add snuba subscriptions-scheduler-executor for eap_items * feat: run process-segment sentry consumer --------- Co-authored-by: Burak Yigit Kaya --- docker-compose.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index c75376c5964..8d66a5f21a2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -305,6 +305,14 @@ services: command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset profiles: - feature-complete + snuba-eap-items-consumer: + <<: *snuba_defaults + command: rust-consumer --storage eap_items --consumer-group eap_items_group --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --use-rust-processor + profiles: + - feature-complete + snuba-subscription-consumer-eap-items: + <<: *snuba_defaults + command: subscriptions-scheduler-executor --dataset events_analytics_platform --entity eap_items --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-eap-items-subscriptions-consumers --followed-consumer-group=eap_items_group --schedule-ttl=60 --stale-threshold-seconds=900 snuba-uptime-results-consumer: <<: *snuba_defaults command: rust-consumer --storage uptime_monitor_checks --consumer-group snuba-uptime-results --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset @@ -410,6 +418,16 @@ services: command: run consumer ingest-feedback-events --consumer-group ingest-feedback profiles: - feature-complete + process-spans: + <<: *sentry_defaults + command: run consumer --no-strict-offset-reset process-spans --consumer-group process-spans + profiles: + - feature-complete + process-segments: + <<: *sentry_defaults + command: run consumer --no-strict-offset-reset process-segments --consumer-group process-segments + profiles: + - feature-complete monitors-clock-tick: <<: *sentry_defaults command: run consumer monitors-clock-tick --consumer-group monitors-clock-tick @@ -440,6 +458,11 @@ services: command: run consumer transactions-subscription-results --consumer-group query-subscription-consumer profiles: - feature-complete + subscription-consumer-eap-items: + <<: *sentry_defaults + command: run consumer subscription-results-eap-items --consumer-group subscription-results-eap-items + profiles: + - feature-complete subscription-consumer-metrics: <<: *sentry_defaults command: run consumer metrics-subscription-results --consumer-group query-subscription-consumer From 0c63bec2433f291c61eaa14ac22ebb896f30e1bd Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 10 Jul 2025 06:27:09 +0700 Subject: [PATCH 215/287] docs: encourage community patches (#3794) Hopefully this will provide a better guide on how to create patches. --- optional-modifications/README.md | 51 ++++++++------- optional-modifications/_lib.sh | 15 ----- .../external-kafka/docker-compose.yml.patch | 62 +++++++++++++++++-- 3 files changed, 86 insertions(+), 42 deletions(-) delete mode 100755 optional-modifications/_lib.sh diff --git a/optional-modifications/README.md b/optional-modifications/README.md index 84cf7e0e2b9..054942138de 100644 --- a/optional-modifications/README.md +++ b/optional-modifications/README.md @@ -1,11 +1,10 @@ # Optional Modifications -Other than the default self-hosted Sentry installation, sometimes users -can leverage their existing infrastructure to help them with limited -resources. "Patches", or you might call this like a "plugin system", is -a collection of patch files (see [man patch(1)](https://man7.org/linux/man-pages/man1/patch.1.html)) -that can be used with to modify the existing configuration to achieve -the desired goal. +While the default self-hosted Sentry installation is often sufficient, there are instances where leveraging existing infrastructure becomes a practical necessity, particularly for users with limited resources. This is where **patches**, or what can be understood as a **plugin system**, come into play. + +A patch system comprises a collection of patch files (refer to man patch(1) for detailed information) designed to modify an existing Sentry configuration. This allows for targeted adjustments to achieve specific operational goals, optimizing Sentry's functionality within your current environment. This approach provides a flexible alternative to a full, customized re-installation, enabling users to adapt Sentry to their specific needs with greater efficiency. + +We also actively encourage the community to contribute! If you've developed a patch that enhances your self-hosted Sentry experience, consider submitting a pull request. Your contributions can be invaluable to other users facing similar challenges, fostering a collaborative environment where shared solutions benefit everyone. > [!WARNING] > Beware that this is very experimental and might not work as expected. @@ -14,28 +13,38 @@ the desired goal. ## How to use patches -The patches are designed mostly to help modify the existing -configuration files. You will need to run the `install.sh` script -afterwards. +The patches are designed mostly to help modify the existing configuration files. You will need to run the `install.sh` script afterwards. -They should be run from the root directory. For example, the -`external-kafka` patches should be run as: +They should be run from the root directory. For example, the `external-kafka` patches should be run as: ```bash -patch < optional-modifications/patches/external-kafka/.env.patch -patch < optional-modifications/patches/external-kafka/config.example.yml.patch -patch < optional-modifications/patches/external-kafka/sentry.conf.example.py.patch -patch < optional-modifications/patches/external-kafka/docker-compose.yml.patch +patch -p0 < optional-modifications/patches/external-kafka/.env.patch +patch -p0 < optional-modifications/patches/external-kafka/config.example.yml.patch +patch -p0 < optional-modifications/patches/external-kafka/sentry.conf.example.py.patch +patch -p0 < optional-modifications/patches/external-kafka/docker-compose.yml.patch ``` -Some patches might require additional steps to be taken, like providing -credentials or additional TLS certificates. +The `-p0` flag is important to ensure the patch applies to the correct absolute file path. + +Some patches might require additional steps to be taken, like providing credentials or additional TLS certificates. Make sure to see your changed files before running the `install.sh` script. + +## How to create patches + +1. Copy the original file to a temporary file name. For example, if you want to create a `clustered-redis` patch, you might want to copy `docker-compose.yml` to `docker-compose.clustered-redis.yml`. +2. Make your changes on the `docker-compose.clustered-redis.yml` file. +3. Run the following command to create the patch: + ```bash + diff -Naru docker-compose.yml docker-compose.clustered-redis.yml > docker-compose.yml.patch + ``` + Or the template command: + ```bash + diff -Naru [original file] [patched file] > [destination file].patch + ``` +4. Create a new directory in the `optional-modifications/patches` folder with the name of the patch. For example, `optional-modifications/patches/clustered-redis`. +5. Move the patched files (like `docker-compose.yml.patch` earlier) into the new directory. ## Official support -Sentry employees are not obliged to provide dedicated support for -patches, but they can help by providing information to move us forward. -We encourage the community to contribute for any bug fixes or -improvements. +While Sentry employees aren't able to offer dedicated support for these patches, they can provide valuable information to help move things forward. Ultimately, we really encourage the community to take the wheel, maintaining and fostering these patches themselves. If you have questions, Sentry employees will be there to help guide you. See the [support policy for self-hosted Sentry](https://develop.sentry.dev/self-hosted/support/) for more information. diff --git a/optional-modifications/_lib.sh b/optional-modifications/_lib.sh deleted file mode 100755 index 46e55d3e257..00000000000 --- a/optional-modifications/_lib.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -test "${DEBUG:-}" && set -x - -function patch_file() { - target="$1" - content="$2" - if [[ -f "$target" ]]; then - echo "🙈 Patching $target ..." - patch -p1 <"$content" - else - echo "🙊 Skipping $target ..." - fi -} diff --git a/optional-modifications/patches/external-kafka/docker-compose.yml.patch b/optional-modifications/patches/external-kafka/docker-compose.yml.patch index cd669acc784..7f2561cce67 100644 --- a/optional-modifications/patches/external-kafka/docker-compose.yml.patch +++ b/optional-modifications/patches/external-kafka/docker-compose.yml.patch @@ -1,5 +1,5 @@ ---- docker-compose.yml 2025-03-17 13:32:15.120328412 +0700 -+++ docker-compose.external-kafka.yml 2025-05-15 08:39:05.509951068 +0700 +--- docker-compose.yml 2025-07-08 10:22:36.600616503 +0700 ++++ docker-compose.external-kafka.yml 2025-07-08 10:36:44.069900011 +0700 @@ -26,8 +26,6 @@ depends_on: redis: @@ -48,7 +48,7 @@ REDIS_HOST: redis UWSGI_MAX_REQUESTS: "10000" UWSGI_DISABLE_LOGGING: "true" -@@ -140,43 +151,7 @@ +@@ -136,43 +147,7 @@ POSTGRES_HOST_AUTH_METHOD: "trust" volumes: - "sentry-postgres:/var/lib/postgresql/data" @@ -93,7 +93,7 @@ clickhouse: <<: *restart_policy image: clickhouse-self-hosted-local -@@ -475,9 +450,8 @@ +@@ -509,9 +484,8 @@ read_only: true source: ./geoip target: /geoip @@ -104,7 +104,32 @@ redis: <<: *depends_on-healthy web: -@@ -486,15 +460,21 @@ +@@ -520,8 +494,22 @@ + <<: *restart_policy + image: "$TASKBROKER_IMAGE" + environment: +- TASKBROKER_KAFKA_CLUSTER: "kafka:9092" +- TASKBROKER_KAFKA_DEADLETTER_CLUSTER: "kafka:9092" ++ TASKBROKER_KAFKA_CLUSTER: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ TASKBROKER_KAFKA_SECURITY_PROTOCOL: ${KAFKA_SECURITY_PROTOCOL:-PLAINTEXT} ++ TASKBROKER_KAFKA_SSL_CA_LOCATION: ${KAFKA_SSL_CA_LOCATION:-} ++ TASKBROKER_KAFKA_SSL_CERTIFICATE_LOCATION: ${KAFKA_SSL_CERTIFICATE_LOCATION:-} ++ TASKBROKER_KAFKA_SSL_KEY_LOCATION: ${KAFKA_SSL_KEY_LOCATION:-} ++ TASKBROKER_KAFKA_SASL_MECHANISM: ${KAFKA_SASL_MECHANISM:-} ++ TASKBROKER_KAFKA_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-} ++ TASKBROKER_KAFKA_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-} ++ TASKBROKER_KAFKA_DEADLETTER_CLUSTER: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ TASKBROKER_KAFKA_DEADLETTER_SECURITY_PROTOCOL: ${KAFKA_SECURITY_PROTOCOL:-PLAINTEXT} ++ TASKBROKER_KAFKA_DEADLETTER_SSL_CA_LOCATION: ${KAFKA_SSL_CA_LOCATION:-} ++ TASKBROKER_KAFKA_DEADLETTER_SSL_CERTIFICATE_LOCATION: ${KAFKA_SSL_CERTIFICATE_LOCATION:-} ++ TASKBROKER_KAFKA_DEADLETTER_SSL_KEY_LOCATION: ${KAFKA_SSL_KEY_LOCATION:-} ++ TASKBROKER_KAFKA_DEADLETTER_SASL_MECHANISM: ${KAFKA_SASL_MECHANISM:-} ++ TASKBROKER_KAFKA_DEADLETTER_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-} ++ TASKBROKER_KAFKA_DEADLETTER_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-} + TASKBROKER_DB_PATH: "/opt/sqlite/taskbroker-activations.sqlite" + volumes: + - sentry-taskbroker:/opt/sqlite +@@ -538,15 +526,21 @@ <<: *restart_policy image: "$VROOM_IMAGE" environment: @@ -131,7 +156,32 @@ profiles: - feature-complete vroom-cleanup: -@@ -523,8 +503,6 @@ +@@ -571,7 +565,14 @@ + image: "$UPTIME_CHECKER_IMAGE" + command: run + environment: +- UPTIME_CHECKER_RESULTS_KAFKA_CLUSTER: kafka:9092 ++ UPTIME_CHECKER_RESULTS_KAFKA_CLUSTER: ${KAFKA_BOOTSTRAP_SERVERS:-kafka:9092} ++ UPTIME_CHECKER_KAFKA_SECURITY_PROTOCOL: ${KAFKA_SECURITY_PROTOCOL:-PLAINTEXT} ++ UPTIME_CHECKER_KAFKA_SSL_CA_LOCATION: ${KAFKA_SSL_CA_LOCATION:-} ++ UPTIME_CHECKER_KAFKA_SSL_CERT_LOCATION: ${KAFKA_SSL_CERTIFICATE_LOCATION:-} ++ UPTIME_CHECKER_KAFKA_SSL_KEY_LOCATION: ${KAFKA_SSL_KEY_LOCATION:-} ++ UPTIME_CHECKER_KAFKA_SASL_MECHANISM: ${KAFKA_SASL_MECHANISM:-} ++ UPTIME_CHECKER_KAFKA_SASL_USERNAME: ${KAFKA_SASL_USERNAME:-} ++ UPTIME_CHECKER_KAFKA_SASL_PASSWORD: ${KAFKA_SASL_PASSWORD:-} + UPTIME_CHECKER_REDIS_HOST: redis://redis:6379 + # Set to `true` will allow uptime checks against private IP addresses + UPTIME_CHECKER_ALLOW_INTERNAL_IPS: "false" +@@ -582,8 +583,6 @@ + # resolver. + #UPTIME_CHECKER_HTTP_CHECKER_DNS_NAMESERVERS: "8.8.8.8,8.8.4.4" + depends_on: +- kafka: +- <<: *depends_on-healthy + redis: + <<: *depends_on-healthy + profiles: +@@ -597,8 +596,6 @@ external: true sentry-redis: external: true From 9d710cda43d3433bc6ac9ce1f96e94f5f4aed093 Mon Sep 17 00:00:00 2001 From: Hubert Deng Date: Thu, 10 Jul 2025 17:42:50 -0700 Subject: [PATCH 216/287] feat(images):Cutover images to ghcr (#3800) cutover to ghcr --- .env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.env b/.env index bb616638144..ee11f6bd67a 100644 --- a/.env +++ b/.env @@ -9,13 +9,13 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=getsentry/sentry:nightly -SNUBA_IMAGE=getsentry/snuba:nightly -RELAY_IMAGE=getsentry/relay:nightly -SYMBOLICATOR_IMAGE=getsentry/symbolicator:nightly -TASKBROKER_IMAGE=getsentry/taskbroker:nightly -VROOM_IMAGE=getsentry/vroom:nightly -UPTIME_CHECKER_IMAGE=getsentry/uptime-checker:nightly +SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly +SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly +RELAY_IMAGE=ghcr.io/getsentry/relay:nightly +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly +VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly +UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 3e9e1ec367c7c45d6eb794e8763009ea83f16eac Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 11 Jul 2025 22:19:14 +0700 Subject: [PATCH 217/287] fix: set harakiri Django option to 30s (#3792) * fix: set harakiri Django option to 30s Closes https://github.com/getsentry/self-hosted/issues/1573 From @guoard and @aminvakil who found the issue of "uWSGI reports full listen queue" was caused mostly by workers taking longer times to finish. For folks with bigger Sentry installation, they might want to increase the `proxy_read_timeout` and `harakiri` values to a longer (acceptable) time. See the GitHub issue linked above for more details. * feat: document 'harakiri' option instead of making it the default * Update sentry.conf.example.py Co-authored-by: Amin Vakil * Update sentry.conf.example.py Co-authored-by: Amin Vakil --------- Co-authored-by: Amin Vakil --- sentry/sentry.conf.example.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index c8a4afbbd21..a082246c734 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -230,6 +230,12 @@ def get_internal_network(): "workers": 3, "threads": 4, "memory-report": False, + # The `harakiri` option terminates requests that take longer than the + # defined amount of time (in seconds) which can help avoid stuck workers + # caused by GIL issues or deadlocks. + # Ensure nginx `proxy_read_timeout` configuration (default: 30) + # on your `nginx.conf` file to be at least 5 seconds longer than this. + # "harakiri": 25, # Some stuff so uwsgi will cycle workers sensibly "max-requests": 100000, "max-requests-delta": 500, From 91b42a9878ecb960116cfe553ccbfdb4cbb4977b Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 14 Jul 2025 20:49:21 +0700 Subject: [PATCH 218/287] feat: Swap `trace-view-v1` feature flag with `visibility-explore-view` (#3801) --- sentry/sentry.conf.example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index a082246c734..19c58ce5ee3 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -304,7 +304,7 @@ def get_internal_network(): "organizations:dashboards-rh-widget", "organizations:metrics-extraction", "organizations:transaction-metrics-extraction", - "organizations:trace-view-v1", + "organizations:visibility-explore-view", "organizations:dynamic-sampling", "projects:custom-inbound-filters", "projects:data-forwarding", From db2363e2185dbd792d736e4c04c6521baecc512e Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 16 Jul 2025 05:58:08 +0000 Subject: [PATCH 219/287] release: 25.7.0 --- .env | 12 ++++++------ CHANGELOG.md | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.env b/.env index ee11f6bd67a..27fcd3202ba 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly -SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly -RELAY_IMAGE=ghcr.io/getsentry/relay:nightly -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly -VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly +SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.7.0 +SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.7.0 +RELAY_IMAGE=ghcr.io/getsentry/relay:25.7.0 +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.7.0 +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.7.0 +VROOM_IMAGE=ghcr.io/getsentry/vroom:25.7.0 UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f90ca37103..ea0deec5a96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 25.7.0 + +### Various fixes & improvements + +- feat: Swap `trace-view-v1` feature flag with `visibility-explore-view` (#3801) by @aldy505 +- fix: set harakiri Django option to 30s (#3792) by @aldy505 +- feat(images):Cutover images to ghcr (#3800) by @hubertdeng123 +- docs: encourage community patches (#3794) by @aldy505 +- feat: run EAP-related containers (#3778) by @aldy505 +- feat(uptime): Enable uptime in self-hosted (#3787) by @evanpurkhiser +- feat: make `system.secret-key` configurable from environment variables (#3783) by @aldy505 +- ci: run tests on arm64 (#3750) by @aldy505 + ## 25.6.2 ### Various fixes & improvements From 0f2748e85c07b53903c421c7e3dbfbb5be2b0ebb Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 16 Jul 2025 06:10:22 +0000 Subject: [PATCH 220/287] build: Set master version to nightly #skip-changelog --- .env | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.env b/.env index 27fcd3202ba..ee11f6bd67a 100644 --- a/.env +++ b/.env @@ -9,12 +9,12 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.7.0 -SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.7.0 -RELAY_IMAGE=ghcr.io/getsentry/relay:25.7.0 -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.7.0 -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.7.0 -VROOM_IMAGE=ghcr.io/getsentry/vroom:25.7.0 +SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly +SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly +RELAY_IMAGE=ghcr.io/getsentry/relay:nightly +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly +VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s From 9fbd722d4449276332de041f9c0d99eb60f80e10 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 19 Jul 2025 12:25:35 +0700 Subject: [PATCH 221/287] feat: inspect docker compose failure on self-hosted e2e action (#3817) It's hard to debug docker compose failure on other repositories since they can't see the `docker compose ps` and `docker compose logs`. One problem occurred here: https://github.com/getsentry/relay/pull/4940 This PR aims to provide both commands if failure happens. --- .github/workflows/test.yml | 6 ------ action.yaml | 11 +++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e02e81eb1f8..016fd7f0b93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,9 +54,3 @@ jobs: uses: './' with: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - - name: Inspect failure - if: failure() - run: | - docker compose ps - docker compose logs diff --git a/action.yaml b/action.yaml index ebc22f4831c..8da5c2966db 100644 --- a/action.yaml +++ b/action.yaml @@ -168,3 +168,14 @@ runs: with: directory: ${{ github.action_path }} token: ${{ inputs.CODECOV_TOKEN }} + + - name: Inspect failure + if: failure() + shell: bash + run: | + echo "::group::Inspect failure - docker compose ps" + docker compose ps + echo "::endgroup::" + echo "::group::Inspect failure - docker compose logs" + docker compose logs + echo "::endgroup::" From a2447aa4805abf0e58ceb0ca3dde5cf4ce7ee48a Mon Sep 17 00:00:00 2001 From: Nikita Korolev <66738864+doc-sheet@users.noreply.github.com> Date: Sun, 20 Jul 2025 03:04:09 +0300 Subject: [PATCH 222/287] Cleanup unused feature flags (#3820) * remove organizations:sso-rippling https://github.com/getsentry/sentry/pull/31515 * remove organizations:metrics-extraction https://github.com/getsentry/sentry/pull/69860 * remove organizations:user-feedback-ingest https://github.com/getsentry/sentry/pull/78097 * remove organizations:user-feedback-replay-clip https://github.com/getsentry/sentry/pull/87771 --------- Co-authored-by: ds --- sentry/sentry.conf.example.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 19c58ce5ee3..0c8bb2b38df 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -291,7 +291,6 @@ def get_internal_network(): "organizations:integrations-issue-sync", "organizations:invite-members", "organizations:sso-basic", - "organizations:sso-rippling", "organizations:sso-saml2", "organizations:performance-view", "organizations:advanced-search", @@ -302,7 +301,6 @@ def get_internal_network(): "organizations:dashboards-mep", "organizations:mep-rollout-flag", "organizations:dashboards-rh-widget", - "organizations:metrics-extraction", "organizations:transaction-metrics-extraction", "organizations:visibility-explore-view", "organizations:dynamic-sampling", @@ -326,8 +324,6 @@ def get_internal_network(): ) # User Feedback related flags + ( - "organizations:user-feedback-ingest", - "organizations:user-feedback-replay-clip", "organizations:user-feedback-ui", ) # Continuous Profiling related flags From d696c202df499de0bcc80acde82baeb5440a99fd Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 22 Jul 2025 18:12:20 +0700 Subject: [PATCH 223/287] fix(action): missing project directory path for failure inspection (#3825) This one is missing. First seen here: https://github.com/getsentry/uptime-checker/actions/runs/16429015462/job/46426515654 --- action.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/action.yaml b/action.yaml index 8da5c2966db..40b76283268 100644 --- a/action.yaml +++ b/action.yaml @@ -174,6 +174,7 @@ runs: shell: bash run: | echo "::group::Inspect failure - docker compose ps" + cd ${{ github.action_path }} docker compose ps echo "::endgroup::" echo "::group::Inspect failure - docker compose logs" From abe34d09ed8c2a24f067bfb00327539a866efb51 Mon Sep 17 00:00:00 2001 From: Daniel Bunte Date: Tue, 22 Jul 2025 15:58:07 +0200 Subject: [PATCH 224/287] feat(install): Adds support for podman(compose) (#3673) --- .github/workflows/test.yml | 16 ++++- docker-compose.yml | 10 +-- install/_detect-container-engine.sh | 12 ++++ install/_min-requirements.sh | 3 + install/build-docker-images.sh | 4 +- install/check-minimum-requirements.sh | 42 ++++++----- install/create-docker-volumes.sh | 23 +++++-- install/dc-detect-version.sh | 69 ++++++++++++++++--- install/detect-platform.sh | 12 ++-- ...ensure-correct-permissions-profiles-dir.sh | 2 + install/error-handling.sh | 8 +-- install/geoip.sh | 2 +- install/parse-cli.sh | 6 +- install/set-up-and-migrate-database.sh | 2 +- install/setup-js-sdk-assets.sh | 2 +- install/turn-things-off.sh | 11 ++- install/update-docker-images.sh | 22 +++--- install/upgrade-clickhouse.sh | 21 ++++-- install/upgrade-postgres.sh | 16 ++--- install/wrap-up.sh | 16 +++-- sentry-admin.sh | 4 +- 21 files changed, 224 insertions(+), 79 deletions(-) create mode 100644 install/_detect-container-engine.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 016fd7f0b93..e66f8668330 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,16 +40,30 @@ jobs: if: github.repository_owner == 'getsentry' runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-24.04, ubuntu-24.04-arm] - name: ${{ matrix.os == 'ubuntu-24.04-arm' && 'integration test (arm64)' || 'integration test' }} + container_engine: ['docker'] # TODO: add 'podman' into the list + name: ${{ matrix.os == 'ubuntu-24.04-arm' && (matrix.container_engine == 'docker' && 'integration test (arm64)' || 'integration test (arm64 podman)') || (matrix.container_engine == 'docker' && 'integration test' || 'integration test (podman)') }} env: REPORT_SELF_HOSTED_ISSUES: 0 SELF_HOSTED_TESTING_DSN: ${{ vars.SELF_HOSTED_TESTING_DSN }} + CONTAINER_ENGINE_PODMAN: ${{ matrix.container_engine == 'podman' && '1' || '0' }} steps: - name: Checkout uses: actions/checkout@v4 + - name: Install Podman + if: matrix.container_engine == 'podman' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends podman + # TODO: Replace below with podman-compose + # We need this commit to be able to work: https://github.com/containers/podman-compose/commit/8206cc3ea277eee6c2e87d4cd66eba8eae3d44eb + pip3 install --user https://github.com/containers/podman-compose/archive/main.tar.gz + echo "PODMAN_COMPOSE_PROVIDER=podman-compose" >> $GITHUB_ENV + echo "PODMAN_COMPOSE_WARNING_LOGS=false" >> $GITHUB_ENV + - name: Use action from local checkout uses: './' with: diff --git a/docker-compose.yml b/docker-compose.yml index 8d66a5f21a2..3373402c58a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,7 @@ x-restart-policy: &restart_policy restart: unless-stopped +x-pull-policy: &pull_policy + pull_policy: never x-depends_on-healthy: &depends_on-healthy condition: service_healthy x-depends_on-default: &depends_on-default @@ -15,7 +17,7 @@ x-healthcheck-defaults: &healthcheck_defaults retries: $HEALTHCHECK_RETRIES start_period: 10s x-sentry-defaults: &sentry_defaults - <<: *restart_policy + <<: [*restart_policy, *pull_policy] image: sentry-self-hosted-local # Set the platform to build for linux/arm64 when needed on Apple silicon Macs. platform: ${DOCKER_PLATFORM:-} @@ -174,7 +176,7 @@ services: timeout: 10s retries: 30 clickhouse: - <<: *restart_policy + <<: [*restart_policy, *pull_policy] image: clickhouse-self-hosted-local build: context: ./clickhouse @@ -329,7 +331,7 @@ services: target: /etc/symbolicator command: run -c /etc/symbolicator/config.yml symbolicator-cleanup: - <<: *restart_policy + <<: [*restart_policy, *pull_policy] image: symbolicator-cleanup-self-hosted-local build: context: ./cron @@ -550,7 +552,7 @@ services: profiles: - feature-complete vroom-cleanup: - <<: *restart_policy + <<: [*restart_policy, *pull_policy] image: vroom-cleanup-self-hosted-local build: context: ./cron diff --git a/install/_detect-container-engine.sh b/install/_detect-container-engine.sh new file mode 100644 index 00000000000..7fc23de2e2e --- /dev/null +++ b/install/_detect-container-engine.sh @@ -0,0 +1,12 @@ +echo "${_group}Detecting container engine ..." + +if [[ "${CONTAINER_ENGINE_PODMAN:-0}" -eq 1 ]] && command -v podman &>/dev/null; then + export CONTAINER_ENGINE="podman" +elif command -v docker &>/dev/null; then + export CONTAINER_ENGINE="docker" +else + echo "FAIL: Neither podman nor docker is installed on the system." + exit 1 +fi +echo "Detected container engine: $CONTAINER_ENGINE" +echo "${_endgroup}" diff --git a/install/_min-requirements.sh b/install/_min-requirements.sh index 17afa00995d..36c5dc0f625 100644 --- a/install/_min-requirements.sh +++ b/install/_min-requirements.sh @@ -2,6 +2,9 @@ MIN_DOCKER_VERSION='19.03.6' MIN_COMPOSE_VERSION='2.32.2' +MIN_PODMAN_VERSION='4.9.3' +MIN_PODMAN_COMPOSE_VERSION='1.3.0' + # 16 GB minimum host RAM, but there'll be some overhead outside of what # can be allotted to docker if [[ "$COMPOSE_PROFILES" == "errors-only" ]]; then diff --git a/install/build-docker-images.sh b/install/build-docker-images.sh index c793b9ea9c8..dce4c43ca4b 100644 --- a/install/build-docker-images.sh +++ b/install/build-docker-images.sh @@ -3,10 +3,10 @@ echo "${_group}Building and tagging Docker images ..." echo "" # Build any service that provides the image sentry-self-hosted-local first, # as it is used as the base image for sentry-cleanup-self-hosted-local. -$dcb --force-rm web +$dcb web # Build each other service individually to localize potential failures better. for service in $($dc config --services); do - $dcb --force-rm "$service" + $dcb "$service" done echo "" echo "Docker images built." diff --git a/install/check-minimum-requirements.sh b/install/check-minimum-requirements.sh index e06db42c48a..322de339759 100644 --- a/install/check-minimum-requirements.sh +++ b/install/check-minimum-requirements.sh @@ -2,31 +2,41 @@ echo "${_group}Checking minimum requirements ..." source install/_min-requirements.sh -DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' || echo '') +DOCKER_VERSION=$($CONTAINER_ENGINE version --format '{{.Server.Version}}' || echo '') if [[ -z "$DOCKER_VERSION" ]]; then - echo "FAIL: Unable to get docker version, is the docker daemon running?" + echo "FAIL: Unable to get $CONTAINER_ENGINE version, is the $CONTAINER_ENGINE daemon running?" exit 1 fi -if ! vergte ${DOCKER_VERSION//v/} $MIN_DOCKER_VERSION; then - echo "FAIL: Expected minimum docker version to be $MIN_DOCKER_VERSION but found $DOCKER_VERSION" - exit 1 -fi -echo "Found Docker version $DOCKER_VERSION" - -if ! vergte ${COMPOSE_VERSION//v/} $MIN_COMPOSE_VERSION; then - echo "FAIL: Expected minimum $dc_base version to be $MIN_COMPOSE_VERSION but found $COMPOSE_VERSION" - exit 1 +if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + if ! vergte ${DOCKER_VERSION//v/} $MIN_DOCKER_VERSION; then + echo "FAIL: Expected minimum docker version to be $MIN_DOCKER_VERSION but found $DOCKER_VERSION" + exit 1 + fi + if ! vergte ${COMPOSE_VERSION//v/} $MIN_COMPOSE_VERSION; then + echo "FAIL: Expected minimum $dc_base version to be $MIN_COMPOSE_VERSION but found $COMPOSE_VERSION" + exit 1 + fi +elif [[ "$CONTAINER_ENGINE" == "podman" ]]; then + if ! vergte ${DOCKER_VERSION//v/} $MIN_PODMAN_VERSION; then + echo "FAIL: Expected minimum podman version to be $MIN_PODMAN_VERSION but found $DOCKER_VERSION" + exit 1 + fi + if ! vergte ${COMPOSE_VERSION//v/} $MIN_PODMAN_COMPOSE_VERSION; then + echo "FAIL: Expected minimum $dc_base version to be $MIN_PODMAN_COMPOSE_VERSION but found $COMPOSE_VERSION" + exit 1 + fi fi -echo "Found Docker Compose version $COMPOSE_VERSION" +echo "Found $CONTAINER_ENGINE version $DOCKER_VERSION" +echo "Found $CONTAINER_ENGINE Compose version $COMPOSE_VERSION" -CPU_AVAILABLE_IN_DOCKER=$(docker run --rm busybox nproc --all) +CPU_AVAILABLE_IN_DOCKER=$($CONTAINER_ENGINE run --rm busybox nproc --all) if [[ "$CPU_AVAILABLE_IN_DOCKER" -lt "$MIN_CPU_HARD" ]]; then echo "FAIL: Required minimum CPU cores available to Docker is $MIN_CPU_HARD, found $CPU_AVAILABLE_IN_DOCKER" exit 1 fi -RAM_AVAILABLE_IN_DOCKER=$(docker run --rm busybox free -m 2>/dev/null | awk '/Mem/ {print $2}') +RAM_AVAILABLE_IN_DOCKER=$($CONTAINER_ENGINE run --rm busybox free -m 2>/dev/null | awk '/Mem/ {print $2}') if [[ "$RAM_AVAILABLE_IN_DOCKER" -lt "$MIN_RAM_HARD" ]]; then echo "FAIL: Required minimum RAM available to Docker is $MIN_RAM_HARD MB, found $RAM_AVAILABLE_IN_DOCKER MB" exit 1 @@ -35,9 +45,9 @@ fi #SSE4.2 required by Clickhouse (https://clickhouse.yandex/docs/en/operations/requirements/) # On KVM, cpuinfo could falsely not report SSE 4.2 support, so skip the check. https://github.com/ClickHouse/ClickHouse/issues/20#issuecomment-226849297 # This may also happen on other virtualization software such as on VMWare ESXi hosts. -IS_KVM=$(docker run --rm busybox grep -c 'Common KVM processor' /proc/cpuinfo || :) +IS_KVM=$($CONTAINER_ENGINE run --rm busybox grep -c 'Common KVM processor' /proc/cpuinfo || :) if [[ ! "$SKIP_SSE42_REQUIREMENTS" -eq 1 && "$IS_KVM" -eq 0 && "$DOCKER_ARCH" = "x86_64" ]]; then - SUPPORTS_SSE42=$(docker run --rm busybox grep -c sse4_2 /proc/cpuinfo || :) + SUPPORTS_SSE42=$($CONTAINER_ENGINE run --rm busybox grep -c sse4_2 /proc/cpuinfo || :) if [[ "$SUPPORTS_SSE42" -eq 0 ]]; then echo "FAIL: The CPU your machine is running on does not support the SSE 4.2 instruction set, which is required for one of the services Sentry uses (Clickhouse). See https://github.com/getsentry/self-hosted/issues/340 for more info." exit 1 diff --git a/install/create-docker-volumes.sh b/install/create-docker-volumes.sh index 15f20d54409..fdbecc2288b 100644 --- a/install/create-docker-volumes.sh +++ b/install/create-docker-volumes.sh @@ -1,10 +1,21 @@ echo "${_group}Creating volumes for persistent storage ..." -echo "Created $(docker volume create --name=sentry-clickhouse)." -echo "Created $(docker volume create --name=sentry-data)." -echo "Created $(docker volume create --name=sentry-kafka)." -echo "Created $(docker volume create --name=sentry-postgres)." -echo "Created $(docker volume create --name=sentry-redis)." -echo "Created $(docker volume create --name=sentry-symbolicator)." +create_volume() { + create_command="$CONTAINER_ENGINE volume create" + if [ "$CONTAINER_ENGINE" = "podman" ]; then + create_command="$create_command --ignore $1" + else + create_command="$create_command --name=$1" + fi + + $create_command +} + +echo "Created $(create_volume sentry-clickhouse)." +echo "Created $(create_volume sentry-data)." +echo "Created $(create_volume sentry-kafka)." +echo "Created $(create_volume sentry-postgres)." +echo "Created $(create_volume sentry-redis)." +echo "Created $(create_volume sentry-symbolicator)." echo "${_endgroup}" diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index 5acaf59c676..6f9d4230d77 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -6,17 +6,27 @@ else _endgroup="" fi -echo "${_group}Initializing Docker Compose ..." +echo "${_group}Initializing Docker|Podman Compose ..." + +export CONTAINER_ENGINE="docker" +if [[ "${CONTAINER_ENGINE_PODMAN:-0}" -eq 1 ]]; then + if command -v podman &>/dev/null; then + export CONTAINER_ENGINE="podman" + else + echo "FAIL: Podman is not installed on the system." + exit 1 + fi +fi # To support users that are symlinking to docker-compose -dc_base="$(docker compose version --short &>/dev/null && echo 'docker compose' || echo '')" -dc_base_standalone="$(docker-compose version --short &>/dev/null && echo 'docker-compose' || echo '')" +dc_base="$(${CONTAINER_ENGINE} compose version --short &>/dev/null && echo "$CONTAINER_ENGINE compose" || echo '')" +dc_base_standalone="$(${CONTAINER_ENGINE}-compose version --short &>/dev/null && echo "$CONTAINER_ENGINE-compose" || echo '')" COMPOSE_VERSION=$([ -n "$dc_base" ] && $dc_base version --short || echo '') STANDALONE_COMPOSE_VERSION=$([ -n "$dc_base_standalone" ] && $dc_base_standalone version --short || echo '') if [[ -z "$COMPOSE_VERSION" && -z "$STANDALONE_COMPOSE_VERSION" ]]; then - echo "FAIL: Docker Compose is required to run self-hosted" + echo "FAIL: Docker|Podman Compose is required to run self-hosted" exit 1 fi @@ -25,14 +35,57 @@ if [[ -z "$COMPOSE_VERSION" ]] || [[ -n "$STANDALONE_COMPOSE_VERSION" ]] && ! ve dc_base="$dc_base_standalone" fi +if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + NO_ANSI="--no-ansi" +else + NO_ANSI="--ansi never" +fi + if [[ "$(basename $0)" = "install.sh" ]]; then - dc="$dc_base --ansi never --env-file ${_ENV}" + dc="$dc_base $NO_ANSI --env-file ${_ENV}" else - dc="$dc_base --ansi never" + dc="$dc_base $NO_ANSI" fi + proxy_args="--build-arg http_proxy=${http_proxy:-} --build-arg https_proxy=${https_proxy:-} --build-arg no_proxy=${no_proxy:-}" -dcr="$dc run --pull=never --rm" +if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + proxy_args_dc="--podman-build-args http_proxy=${http_proxy:-},https_proxy=${https_proxy:-},no_proxy=${no_proxy:-}" + # Disable pod creation as these are one-off commands and creating a pod + # prints its pod id to stdout which is messing with the output that we + # rely on various places such as configuration generation + dcr="$dc --profile=feature-complete --in-pod=false run --rm" +else + proxy_args_dc=$proxy_args + dcr="$dc run --pull=never --rm" +fi dcb="$dc build $proxy_args" -dbuild="docker build $proxy_args" +dbuild="$CONTAINER_ENGINE build $proxy_args" echo "$dcr" +# Utility function to handle --wait with docker and podman +function start_service_and_wait_ready() { + local options=() + local services=() + local found_service=0 + + for arg in "$@"; do + if [[ $found_service -eq 0 && "$arg" == -* ]]; then + options+=("$arg") + else + found_service=1 + services+=("$arg") + fi + done + + if [ "$CONTAINER_ENGINE" = "podman" ]; then + $dc up --force-recreate -d "${options[@]}" "${services[@]}" + for service in "${services[@]}"; do + while ! $CONTAINER_ENGINE ps --filter "health=healthy" | grep "$service"; do + sleep 2 + done + done + else + $dc up --wait "${options[@]}" "${services[@]}" + fi +} + echo "${_endgroup}" diff --git a/install/detect-platform.sh b/install/detect-platform.sh index 7404008f41c..9009f79b63d 100644 --- a/install/detect-platform.sh +++ b/install/detect-platform.sh @@ -1,3 +1,5 @@ +source install/_detect-container-engine.sh + echo "${_group}Detecting Docker platform" # Sentry SaaS uses stock Yandex ClickHouse, but they don't provide images that @@ -12,13 +14,13 @@ echo "${_group}Detecting Docker platform" # linux/amd64 by default due to virtualization. # See https://github.com/docker/cli/issues/3286 for the Docker bug. -if ! command -v docker &>/dev/null; then - echo "FAIL: Could not find a \`docker\` binary on this system. Are you sure it's installed?" - exit 1 +FORMAT="{{.Architecture}}" +if [[ $CONTAINER_ENGINE == "podman" ]]; then + FORMAT="{{.Host.Arch}}" fi -export DOCKER_ARCH=$(docker info --format '{{.Architecture}}') -if [[ "$DOCKER_ARCH" = "x86_64" ]]; then +export DOCKER_ARCH=$($CONTAINER_ENGINE info --format "$FORMAT") +if [[ "$DOCKER_ARCH" = "x86_64" || "$DOCKER_ARCH" = "amd64" ]]; then export DOCKER_PLATFORM="linux/amd64" elif [[ "$DOCKER_ARCH" = "aarch64" ]]; then export DOCKER_PLATFORM="linux/arm64" diff --git a/install/ensure-correct-permissions-profiles-dir.sh b/install/ensure-correct-permissions-profiles-dir.sh index 68b782bc8a5..8c590b3aeba 100755 --- a/install/ensure-correct-permissions-profiles-dir.sh +++ b/install/ensure-correct-permissions-profiles-dir.sh @@ -3,5 +3,7 @@ # TODO: Remove this after the next hard-stop echo "${_group}Ensuring correct permissions on profiles directory ..." + $dcr --no-deps --entrypoint /bin/bash --user root vroom -c 'chown -R vroom:vroom /var/vroom/sentry-profiles && chmod -R o+rwx /var/vroom/sentry-profiles' + echo "${_endgroup}" diff --git a/install/error-handling.sh b/install/error-handling.sh index cbd0676858a..09aa1c2fe13 100644 --- a/install/error-handling.sh +++ b/install/error-handling.sh @@ -6,8 +6,8 @@ fi $dbuild -t sentry-self-hosted-jq-local --platform="$DOCKER_PLATFORM" jq -jq="docker run --rm -i sentry-self-hosted-jq-local" -sentry_cli="docker run --rm -v /tmp:/work -e SENTRY_DSN=$SENTRY_DSN getsentry/sentry-cli" +jq="$CONTAINER_ENGINE run --rm -i sentry-self-hosted-jq-local" +sentry_cli="$CONTAINER_ENGINE run --rm -v /tmp:/work -e SENTRY_DSN=$SENTRY_DSN getsentry/sentry-cli" send_envelope() { # Send envelope @@ -27,7 +27,7 @@ send_event() { local breadcrumbs=$5 local fingerprint_value=$( echo -n "$cmd_exit $error_msg $traceback" | - docker run -i --rm busybox md5sum | + $CONTAINER_ENGINE run -i --rm busybox md5sum | cut -d' ' -f1 ) local envelope_file="sentry-envelope-${fingerprint_value}" @@ -151,7 +151,7 @@ fi # Make sure we can use sentry-cli if we need it. if [ "$REPORT_SELF_HOSTED_ISSUES" == 1 ]; then - if ! docker pull getsentry/sentry-cli:latest; then + if ! $CONTAINER_ENGINE pull getsentry/sentry-cli:latest; then echo "Failed to pull sentry-cli, won't report to Sentry after all." export REPORT_SELF_HOSTED_ISSUES=0 fi diff --git a/install/geoip.sh b/install/geoip.sh index 041db9b6833..0d1b2efc0aa 100644 --- a/install/geoip.sh +++ b/install/geoip.sh @@ -21,7 +21,7 @@ install_geoip() { else echo "IP address geolocation is configured for updates." echo "Updating IP address geolocation database ... " - if ! docker run --rm -v "./geoip:/sentry" --entrypoint '/usr/bin/geoipupdate' "ghcr.io/maxmind/geoipupdate:v6.1.0" "-d" "/sentry" "-f" "/sentry/GeoIP.conf"; then + if ! $CONTAINER_ENGINE run --rm -v "./geoip:/sentry" --entrypoint '/usr/bin/geoipupdate' "ghcr.io/maxmind/geoipupdate:v6.1.0" "-d" "/sentry" "-f" "/sentry/GeoIP.conf"; then result='Error' fi echo "$result updating IP address geolocation database." diff --git a/install/parse-cli.sh b/install/parse-cli.sh index 0390b54f9e9..b342033ca08 100644 --- a/install/parse-cli.sh +++ b/install/parse-cli.sh @@ -4,7 +4,7 @@ show_help() { cat <stdout redirection below and pass it through grep, ignoring all lines -# having this '-onpremise-local' suffix. +if [ "$CONTAINER_ENGINE" = "podman" ]; then + # podman compose doesn't have the --ignore-pull-failures option, so can just + # run the command normally + $dc --profile feature-complete pull || true +else + # We tag locally built images with a '-self-hosted-local' suffix. `docker + # compose pull` tries to pull these too and shows a 404 error on the console + # which is confusing and unnecessary. To overcome this, we add the + # stderr>stdout redirection below and pass it through grep, ignoring all lines + # having this '-onpremise-local' suffix. -$dc pull -q --ignore-pull-failures 2>&1 | grep -v -- -self-hosted-local || true + $dc pull --ignore-pull-failures 2>&1 | grep -v -- -self-hosted-local || true +fi # We may not have the set image on the repo (local images) so allow fails -docker pull ${SENTRY_IMAGE} || true +$CONTAINER_ENGINE pull ${SENTRY_IMAGE} || true echo "${_endgroup}" diff --git a/install/upgrade-clickhouse.sh b/install/upgrade-clickhouse.sh index 3dcb56f189c..93456b8ddbe 100644 --- a/install/upgrade-clickhouse.sh +++ b/install/upgrade-clickhouse.sh @@ -1,19 +1,28 @@ echo "${_group}Upgrading Clickhouse ..." # First check to see if user is upgrading by checking for existing clickhouse volume -if $dc ps -a | grep -q clickhouse; then +if [ "$CONTAINER_ENGINE" = "podman" ]; then + ps_command="$dc ps" + build_arg="--podman-build-args" +else + # docker compose needs to be run with the -a flag to show all containers + ps_command="$dc ps -a" + build_arg="--build-arg" +fi + +if $ps_command | grep -q clickhouse; then # Start clickhouse if it is not already running - $dc up --wait clickhouse + start_service_and_wait_ready clickhouse # In order to get to 23.8, we need to first upgrade go from 21.8 -> 22.8 -> 23.3 -> 23.8 version=$($dc exec clickhouse clickhouse-client -q 'SELECT version()') if [[ "$version" == "21.8.13.1.altinitystable" || "$version" == "21.8.12.29.altinitydev.arm" ]]; then $dc down clickhouse - $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:22.8.15.25.altinitystable clickhouse - $dc up --wait clickhouse + $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:22.8.15.25.altinitystable clickhouse + start_service_and_wait_ready clickhouse $dc down clickhouse - $dcb --build-arg BASE_IMAGE=altinity/clickhouse-server:23.3.19.33.altinitystable clickhouse - $dc up --wait clickhouse + $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:23.3.19.33.altinitystable clickhouse + start_service_and_wait_ready clickhouse else echo "Detected clickhouse version $version. Skipping upgrades!" fi diff --git a/install/upgrade-postgres.sh b/install/upgrade-postgres.sh index fa66a0aa4ab..6785ae879ea 100644 --- a/install/upgrade-postgres.sh +++ b/install/upgrade-postgres.sh @@ -1,26 +1,26 @@ echo "${_group}Ensuring proper PostgreSQL version ..." -if [[ -n "$(docker volume ls -q --filter name=sentry-postgres)" && "$(docker run --rm -v sentry-postgres:/db busybox cat /db/PG_VERSION 2>/dev/null)" == "9.6" ]]; then - docker volume rm sentry-postgres-new || true +if [[ -n "$($CONTAINER_ENGINE volume ls -q --filter name=sentry-postgres)" && "$($CONTAINER_ENGINE run --rm -v sentry-postgres:/db busybox cat /db/PG_VERSION 2>/dev/null)" == "9.6" ]]; then + $CONTAINER_ENGINE volume rm sentry-postgres-new || true # If this is Postgres 9.6 data, start upgrading it to 14.0 in a new volume - docker run --rm \ + $CONTAINER_ENGINE run --rm \ -v sentry-postgres:/var/lib/postgresql/9.6/data \ -v sentry-postgres-new:/var/lib/postgresql/14/data \ tianon/postgres-upgrade:9.6-to-14 # Get rid of the old volume as we'll rename the new one to that - docker volume rm sentry-postgres - docker volume create --name sentry-postgres + $CONTAINER_ENGINE volume rm sentry-postgres + $CONTAINER_ENGINE volume create --name sentry-postgres # There's no rename volume in Docker so copy the contents from old to new name # Also append the `host all all all trust` line as `tianon/postgres-upgrade:9.6-to-14` # doesn't do that automatically. - docker run --rm -v sentry-postgres-new:/from -v sentry-postgres:/to alpine ash -c \ + $CONTAINER_ENGINE run --rm -v sentry-postgres-new:/from -v sentry-postgres:/to alpine ash -c \ "cd /from ; cp -av . /to ; echo 'host all all all trust' >> /to/pg_hba.conf" # Finally, remove the new old volume as we are all in sentry-postgres now. - docker volume rm sentry-postgres-new + $CONTAINER_ENGINE volume rm sentry-postgres-new echo "Re-indexing due to glibc change, this may take a while..." echo "Starting up new PostgreSQL version" - $dc up --wait postgres + start_service_and_wait_ready postgres # Wait for postgres RETRIES=5 diff --git a/install/wrap-up.sh b/install/wrap-up.sh index 6f242284fe6..52e31137d84 100644 --- a/install/wrap-up.sh +++ b/install/wrap-up.sh @@ -2,15 +2,15 @@ if [[ "$MINIMIZE_DOWNTIME" ]]; then echo "${_group}Waiting for Sentry to start ..." # Start the whole setup, except nginx and relay. - $dc up --wait --remove-orphans $($dc config --services | grep -v -E '^(nginx|relay)$') + start_service_and_wait_ready --remove-orphans $($dc config --services | grep -v -E '^(nginx|relay)$') $dc restart relay $dc exec -T nginx nginx -s reload - docker run --rm --network="${COMPOSE_PROJECT_NAME}_default" alpine ash \ + $CONTAINER_ENGINE run --rm --network="${COMPOSE_PROJECT_NAME}_default" alpine ash \ -c 'while [[ "$(wget -T 1 -q -O- http://web:9000/_health/)" != "ok" ]]; do sleep 0.5; done' # Make sure everything is up. This should only touch relay and nginx - $dc up --wait + start_service_and_wait_ready $($dc config --services) echo "${_endgroup}" else @@ -22,7 +22,15 @@ else if [[ "${_ENV}" =~ ".env.custom" ]]; then echo " $dc_base --env-file .env --env-file ${_ENV} up --wait" else - echo " $dc_base up --wait" + if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + if [[ "$COMPOSE_PROFILES" == "feature-complete" ]]; then + echo " $dc_base --profile=feature-complete up --force-recreate -d" + else + echo " $dc_base up --force-recreate -d" + fi + else + echo " $dc_base up --wait" + fi fi echo "" echo "-----------------------------------------------------------------" diff --git a/sentry-admin.sh b/sentry-admin.sh index f90af33c81a..689e7fab14f 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -23,8 +23,8 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { - $dc up postgres --wait - $dc up redis --wait + start_service_and_wait_ready postgres + start_service_and_wait_ready redis --wait $dcr --no-deps -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 } From 79f18eeec3f61ac60f6b9a4015b16eaeb165d8a5 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 25 Jul 2025 10:19:52 +0700 Subject: [PATCH 225/287] docs: clearly state that `system.internal-url-prefix` shouldn't be changed (#3829) * docs: clearly state that `system.internal-url-prefix` shouldn't be changed Happened today on Discord where a user changed internal-url-prefix and broke their instance. I think this should be stated clearer for new users and for non-native English speakers. * Apply suggestion from @BYK Co-authored-by: Burak Yigit Kaya * Apply suggestion from @aldy505 Co-authored-by: Reinaldy Rafli * Apply changes from @BYK --------- Co-authored-by: Burak Yigit Kaya --- sentry/config.example.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/sentry/config.example.yml b/sentry/config.example.yml index 3c499e7a625..fefe9511b81 100644 --- a/sentry/config.example.yml +++ b/sentry/config.example.yml @@ -45,8 +45,18 @@ mail.host: 'smtp' # System Settings # ################### -# The URL prefix in which Sentry is accessible +# This is the main URL prefix where Sentry can be accessed. +# Sentry will use this to create links to its different parts of the web UI. +# This is most helpful if you are using an external reverse proxy. # system.url-prefix: https://example.sentry.com + +# Most of the time, this should NOT be changed. It's used for communication +# between containers. `web` is the container's name, and `9000` is the +# default port opened by the Sentry backend (this is NOT the public port). +# +# If you want to change the publicly exposed domain or port, you should change +# `system.url-prefix` above instead, along with `SENTRY_BIND` in `.env` file. +# Also see https://develop.sentry.dev/self-hosted/#productionalizing. system.internal-url-prefix: 'http://web:9000/' # If this file ever becomes compromised, it's important to generate a new key. From 524b8d4db085f960afe1544c90dda7762cd5f747 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sun, 27 Jul 2025 08:57:01 +0700 Subject: [PATCH 226/287] Potential fix for code scanning alert no. 12: Workflow does not contain permissions (#3822) Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e66f8668330..1ad0a1c4c8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,3 +1,5 @@ +permissions: + contents: read name: Test on: # Run CI on all pushes to the master and release/** branches, and on all new From 84d0d23af899d90a2adf2451d247f545a1549246 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 29 Jul 2025 17:17:23 +0700 Subject: [PATCH 227/287] feat(features): add `profiling-view` flag (#3837) Closes https://github.com/getsentry/sentry/issues/95752 --- sentry/sentry.conf.example.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 0c8bb2b38df..bedb79ff822 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -297,6 +297,7 @@ def get_internal_network(): "organizations:session-replay", "organizations:issue-platform", "organizations:profiling", + "organizations:profiling-view", "organizations:monitors", "organizations:dashboards-mep", "organizations:mep-rollout-flag", From 3f48e35813edf22ae8dee170ea6645900440b117 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sun, 3 Aug 2025 21:07:35 -0400 Subject: [PATCH 228/287] feat: Continue using celery in self-hosted for now (#3845) Disable taskworkers for self-hosted until the documentation has been published. Refs getsentry/sentry#96966 --- sentry/sentry.conf.example.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index bedb79ff822..ee3cc5f93f8 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -442,3 +442,9 @@ def get_internal_network(): # } # SENTRY_METRICS_SAMPLE_RATE = 1.0 # Adjust this to your needs, default is 1.0 # SENTRY_METRICS_PREFIX = "sentry." # Adjust this to your needs, default is "sentry." + +######### +# Tasks # +######### +# Disable taskworker and continue using celery. +SENTRY_OPTIONS["taskworker.enabled"] = False From 29000f157c8563c2263eb902fc165080eadf7a6c Mon Sep 17 00:00:00 2001 From: mzglinski Date: Wed, 6 Aug 2025 00:35:30 +0200 Subject: [PATCH 229/287] fix: add schedulers for generic metrics subscriptions (#3847) --- docker-compose.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3373402c58a..f29a9bfd56f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -267,6 +267,26 @@ services: command: subscriptions-scheduler-executor --dataset metrics --entity metrics_sets --entity metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-metrics-subscriptions-consumers --followed-consumer-group=snuba-metrics-consumers --schedule-ttl=60 --stale-threshold-seconds=900 profiles: - feature-complete + snuba-subscription-consumer-generic-metrics-distributions: + <<: *snuba_defaults + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_distributions --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-distributions-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-distributions-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + profiles: + - feature-complete + snuba-subscription-consumer-generic-metrics-sets: + <<: *snuba_defaults + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_sets --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-sets-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-sets-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + profiles: + - feature-complete + snuba-subscription-consumer-generic-metrics-counters: + <<: *snuba_defaults + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-counters-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-counters-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + profiles: + - feature-complete + snuba-subscription-consumer-generic-metrics-gauges: + <<: *snuba_defaults + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_gauges --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-gauges-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-gauges-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + profiles: + - feature-complete snuba-generic-metrics-distributions-consumer: <<: *snuba_defaults command: rust-consumer --storage generic_metrics_distributions_raw --consumer-group snuba-gen-metrics-distributions-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset From d57613714caf1c5cac77531e20925de8c4a75c01 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 6 Aug 2025 07:05:36 +0700 Subject: [PATCH 230/287] chore(features): cleanup feature flags grouped by its' category (#3843) --- sentry/sentry.conf.example.py | 39 +++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index ee3cc5f93f8..826f43ae2d6 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -279,6 +279,13 @@ def get_internal_network(): # Features # ############ +# Sentry uses feature flags to enable certain features. Some features may +# require additional configuration or containers. To learn more about how +# Sentry uses feature flags, see https://develop.sentry.dev/backend/application-domains/feature-flags/ +# +# The features listed here are stable and generally available on SaaS. +# To enable preview features, see https://develop.sentry.dev/self-hosted/configuration/#enabling-preview-features + SENTRY_FEATURES["projects:sample-events"] = False SENTRY_FEATURES.update( { @@ -292,18 +299,12 @@ def get_internal_network(): "organizations:invite-members", "organizations:sso-basic", "organizations:sso-saml2", - "organizations:performance-view", "organizations:advanced-search", - "organizations:session-replay", "organizations:issue-platform", - "organizations:profiling", - "organizations:profiling-view", "organizations:monitors", "organizations:dashboards-mep", "organizations:mep-rollout-flag", "organizations:dashboards-rh-widget", - "organizations:transaction-metrics-extraction", - "organizations:visibility-explore-view", "organizations:dynamic-sampling", "projects:custom-inbound-filters", "projects:data-forwarding", @@ -312,8 +313,11 @@ def get_internal_network(): "projects:rate-limits", "projects:servicehooks", ) - # Starfish related flags + # Performance/Tracing/Spans related flags + ( + "organizations:performance-view", + "organizations:visibility-explore-view", + "organizations:transaction-metrics-extraction", "organizations:indexed-spans-extraction", "organizations:insights-entry-points", "organizations:insights-initial-modules", @@ -323,33 +327,32 @@ def get_internal_network(): "projects:span-metrics-extraction", "projects:span-metrics-extraction-addons", ) + # Session Replay related flags + + ( + "organizations:session-replay", + ) # User Feedback related flags + ( "organizations:user-feedback-ui", ) + # Profiling related flags + + ( + "organizations:profiling", + "organizations:profiling-view", + ) # Continuous Profiling related flags + ( "organizations:continuous-profiling", "organizations:continuous-profiling-stats", ) - # Uptime related flags + # Uptime Monitoring related flags + ( "organizations:uptime", "organizations:uptime-create-issues", - # TODO(epurkhiser): We can remove remove these in 25.8.0 since - # we'll have released this issue group type - # (https://github.com/getsentry/sentry/pull/94827) - "organizations:issue-uptime-domain-failure-visible", - "organizations:issue-uptime-domain-failure-ingest", - "organizations:issue-uptime-domain-failure-post-process-group", ) } ) -# TODO(epurkhiser): In 25.8.0 we can drop this option override as we've made it -# default in sentry (https://github.com/getsentry/sentry/pull/94822) -SENTRY_OPTIONS["uptime.snuba_uptime_results.enabled"] = True - ####################### # MaxMind Integration # ####################### From a36deff0af00f3b895120ed294ffdea8f23e1ccf Mon Sep 17 00:00:00 2001 From: Iven Schlenther Date: Wed, 6 Aug 2025 11:44:23 +0200 Subject: [PATCH 231/287] fix(enhancement): ensure correct ownership check before setting permissions of profiles (#3855) --- install/ensure-correct-permissions-profiles-dir.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/install/ensure-correct-permissions-profiles-dir.sh b/install/ensure-correct-permissions-profiles-dir.sh index 8c590b3aeba..9f784c86b2b 100755 --- a/install/ensure-correct-permissions-profiles-dir.sh +++ b/install/ensure-correct-permissions-profiles-dir.sh @@ -4,6 +4,11 @@ echo "${_group}Ensuring correct permissions on profiles directory ..." -$dcr --no-deps --entrypoint /bin/bash --user root vroom -c 'chown -R vroom:vroom /var/vroom/sentry-profiles && chmod -R o+rwx /var/vroom/sentry-profiles' +# Check if the parent directory of /var/vroom/sentry-profiles is already owned by vroom:vroom +if [ "$(stat -c '%U:%G' /var/vroom)" = "vroom:vroom" ]; then + echo "Ownership of /var/vroom is already set to vroom:vroom. Skipping chown." +else + $dcr --no-deps --entrypoint /bin/bash --user root vroom -c 'chown -R vroom:vroom /var/vroom/sentry-profiles && chmod -R o+rwx /var/vroom/sentry-profiles' +fi echo "${_endgroup}" From abe68ba370d8b9e1b3f0ea5bbc181044ae78be23 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 6 Aug 2025 22:56:47 +0700 Subject: [PATCH 232/287] fix: uptime checker image should be bumped to the tagged release (#3858) --- scripts/bump-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index f5853a3e2f4..df69deb71a0 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -4,7 +4,7 @@ set -eu OLD_VERSION="$1" NEW_VERSION="$2" -sed -i -e "s/^\(SENTRY\|SNUBA\|RELAY\|SYMBOLICATOR\|TASKBROKER\|VROOM\)_IMAGE=\([^:]\+\):.\+\$/\1_IMAGE=\2:$NEW_VERSION/" .env +sed -i -e "s/^\(SENTRY\|SNUBA\|RELAY\|SYMBOLICATOR\|TASKBROKER\|VROOM\|UPTIME_CHECKER\)_IMAGE=\([^:]\+\):.\+\$/\1_IMAGE=\2:$NEW_VERSION/" .env sed -i -e "s/^\# Self-Hosted Sentry .*/# Self-Hosted Sentry $NEW_VERSION/" README.md [ -z "$OLD_VERSION" ] || echo "Previous version: $OLD_VERSION" From 213423f9d9465a3a679cf20a6c06229a17af7dbd Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 7 Aug 2025 10:14:02 +0700 Subject: [PATCH 233/287] fix(scripts): every known flags should be shifted before executing the sentry command (#3831) Fixes https://github.com/getsentry/self-hosted/issues/3526 Previously, if this was executed: ```bash ./scripts/backup.sh --no-report-self-hosted-issues ``` The final backup command that will be executed is: ```bash docker compose run -v "${PWD}/sentry:/sentry-data/backup" --rm -T -e SENTRY_LOG_LEVEL=CRITICAL web export --no-report-self-hosted-issues /sentry-data/backup/backup.json ``` Which is invalid. This PR strips all known flags specific to self-hosted before passing it onto Sentry. One other option is to enable "ignore unknown options" on click (the CLI dependency) on Sentry, but I don't want to go to that route. --- scripts/_lib.sh | 11 +---------- scripts/backup.sh | 13 +++++++++++++ scripts/reset.sh | 13 +++++++++++++ scripts/restore.sh | 13 +++++++++++++ 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/scripts/_lib.sh b/scripts/_lib.sh index ba57fc42a33..a381b6e585d 100755 --- a/scripts/_lib.sh +++ b/scripts/_lib.sh @@ -78,7 +78,6 @@ function restore() { } # Needed variables to source error-handling script -MINIMIZE_DOWNTIME="${MINIMIZE_DOWNTIME:-}" export STOP_TIMEOUT=60 # Save logs in order to send envelope to Sentry @@ -86,16 +85,8 @@ log_file="sentry_${cmd%% *}_log-$(date +%Y-%m-%d_%H-%M-%S).txt" exec &> >(tee -a "$log_file") version="" -while (($#)); do - case "$1" in - --report-self-hosted-issues) export REPORT_SELF_HOSTED_ISSUES=1 ;; - --no-report-self-hosted-issues) export REPORT_SELF_HOSTED_ISSUES=0 ;; - *) version=$1 ;; - esac - shift -done - # Source files needed to set up error-handling +source install/_lib.sh source install/dc-detect-version.sh source install/detect-platform.sh source install/error-handling.sh diff --git a/scripts/backup.sh b/scripts/backup.sh index c056b7078bc..c92ee4b9ace 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -1,4 +1,17 @@ #!/usr/bin/env bash +MINIMIZE_DOWNTIME="${MINIMIZE_DOWNTIME:-}" +REPORT_SELF_HOSTED_ISSUES="${REPORT_SELF_HOSTED_ISSUES:-}" + +while (($#)); do + case "$1" in + --report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=1 ;; + --no-report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=0 ;; + --minimize-downtime) MINIMIZE_DOWNTIME=1 ;; + *) version=$1 ;; + esac + shift +done + cmd="backup $1" source scripts/_lib.sh $cmd diff --git a/scripts/reset.sh b/scripts/reset.sh index f519435420f..75c244efedc 100755 --- a/scripts/reset.sh +++ b/scripts/reset.sh @@ -1,4 +1,17 @@ #!/usr/bin/env bash +MINIMIZE_DOWNTIME="${MINIMIZE_DOWNTIME:-}" +REPORT_SELF_HOSTED_ISSUES="${REPORT_SELF_HOSTED_ISSUES:-}" + +while (($#)); do + case "$1" in + --report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=1 ;; + --no-report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=0 ;; + --minimize-downtime) MINIMIZE_DOWNTIME=1 ;; + *) version=$1 ;; + esac + shift +done + cmd=reset source scripts/_lib.sh $cmd diff --git a/scripts/restore.sh b/scripts/restore.sh index ae3666b0ec6..57cab1bb60c 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -1,4 +1,17 @@ #!/usr/bin/env bash +MINIMIZE_DOWNTIME="${MINIMIZE_DOWNTIME:-}" +REPORT_SELF_HOSTED_ISSUES="${REPORT_SELF_HOSTED_ISSUES:-}" + +while (($#)); do + case "$1" in + --report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=1 ;; + --no-report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=0 ;; + --minimize-downtime) MINIMIZE_DOWNTIME=1 ;; + *) version=$1 ;; + esac + shift +done + cmd="restore $1" source scripts/_lib.sh From 2b549baee767c2dd7b0f3ccd3caf2bf1c073cfc3 Mon Sep 17 00:00:00 2001 From: Kamil Monicz Date: Sun, 10 Aug 2025 02:48:47 +0200 Subject: [PATCH 234/287] fix(scripts): use `env` to find `bash` interpreter (#3861) --- install/ensure-correct-permissions-profiles-dir.sh | 2 +- scripts/bump-version.sh | 2 +- scripts/post-release.sh | 2 +- sentry-admin.sh | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/install/ensure-correct-permissions-profiles-dir.sh b/install/ensure-correct-permissions-profiles-dir.sh index 9f784c86b2b..5bdadecc70c 100755 --- a/install/ensure-correct-permissions-profiles-dir.sh +++ b/install/ensure-correct-permissions-profiles-dir.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # TODO: Remove this after the next hard-stop diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index df69deb71a0..64ff9732bf9 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu OLD_VERSION="$1" diff --git a/scripts/post-release.sh b/scripts/post-release.sh index 820d8ef6d69..f2e6a238d16 100755 --- a/scripts/post-release.sh +++ b/scripts/post-release.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu # Bring master back to nightlies after merge from release branch diff --git a/sentry-admin.sh b/sentry-admin.sh index 689e7fab14f..aeea2b36845 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Set the script directory as working directory. cd $(dirname $0) From d3c0ea8250de4b987ab6f9dc09177a069da247d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:38:01 +0100 Subject: [PATCH 235/287] build(deps): bump actions/create-github-app-token from 2.0.6 to 2.1.0 (#3865) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.0.6 to 2.1.0. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/df432ceedc7162793a195dd1713ff69aefc7379e...0f859bf9e69e887678d5bbfbee594437cb440ffe) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 2.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d744f50290a..87699d3b48d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 + uses: actions/create-github-app-token@0f859bf9e69e887678d5bbfbee594437cb440ffe # v2.1.0 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 8bf5663c0a11e10a5e63fab376106a3af4f79691 Mon Sep 17 00:00:00 2001 From: Pierre Massat Date: Tue, 12 Aug 2025 12:48:43 -0700 Subject: [PATCH 236/287] fix(eap): Fix dataset parameter to target spans (#3866) --- _integration-test/test_01_basics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index 14ffa253067..b759f629730 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -180,7 +180,9 @@ def test_custom_certificate_authorities(): .public_key(ca_key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.datetime.now(datetime.timezone.utc)) - .not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1)) + .not_valid_after( + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1) + ) .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) .add_extension( x509.KeyUsage( @@ -398,7 +400,7 @@ def placeholder_fn(): lambda x: len(json.loads(x)["data"]) > 0, ) poll_for_response( - f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=spansIndexed&field=id&project=1&statsPeriod=1h", + f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=spans&field=id&project=1&statsPeriod=1h", client, lambda x: len(json.loads(x)["data"]) > 0, ) From 43d4c5df0e036ffa417ce2087fe37abbe20a09e7 Mon Sep 17 00:00:00 2001 From: mzglinski Date: Wed, 13 Aug 2025 05:51:56 +0200 Subject: [PATCH 237/287] feat: healthchecks for sentry components (#3859) * feat: add snuba healthcheck * fix: snuba healthchecks * fix: remove healthcheck from replacer * feat: add healthchecks to sentry * fix: small changes to sentry healthchecks * fix: worker healthcheck * feat: vroom healthcheck * feat: symbolicator healthcheck * feat: add nginx healthcheck * fix: typo in .env * fix: typo in docker-compose.yml * test: increase docker compose --wait-timeout * Revert "test: increase docker compose --wait-timeout" This reverts commit fc43389793017f8041fc1cdae0a5cb5e2fb759f2. * fix: do not use healthcheck: [disabled: true], since it breaks docker compose wait command --------- Co-authored-by: Reinaldy Rafli --- .env | 5 + docker-compose.yml | 246 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 200 insertions(+), 51 deletions(-) diff --git a/.env b/.env index ee11f6bd67a..6570bf8c28f 100644 --- a/.env +++ b/.env @@ -19,6 +19,11 @@ UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 +HEALTHCHECK_START_PERIOD=10s +HEALTHCHECK_FILE_INTERVAL=60s +HEALTHCHECK_FILE_TIMEOUT=10s +HEALTHCHECK_FILE_RETRIES=1 +HEALTHCHECK_FILE_START_PERIOD=180s # Caution: Raising max connections of postgres increases CPU and RAM usage # see https://github.com/getsentry/self-hosted/pull/2740 for more information POSTGRES_MAX_CONNECTIONS=100 diff --git a/docker-compose.yml b/docker-compose.yml index f29a9bfd56f..437bdc7694c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,13 @@ x-healthcheck-defaults: &healthcheck_defaults interval: "$HEALTHCHECK_INTERVAL" timeout: "$HEALTHCHECK_TIMEOUT" retries: $HEALTHCHECK_RETRIES - start_period: 10s + start_period: "$HEALTHCHECK_START_PERIOD" +x-file-healthcheck: &file_healthcheck_defaults + test: ["CMD-SHELL", "rm /tmp/health.txt"] + interval: "$HEALTHCHECK_FILE_INTERVAL" + timeout: "$HEALTHCHECK_FILE_TIMEOUT" + retries: $HEALTHCHECK_FILE_RETRIES + start_period: "$HEALTHCHECK_FILE_START_PERIOD" x-sentry-defaults: &sentry_defaults <<: [*restart_policy, *pull_policy] image: sentry-self-hosted-local @@ -211,133 +217,193 @@ services: retries: 30 snuba-api: <<: *snuba_defaults + healthcheck: + <<: *healthcheck_defaults + test: + - "CMD" + - "/bin/bash" + - "-c" + # Courtesy of https://unix.stackexchange.com/a/234089/108960 + - 'exec 3<>/dev/tcp/127.0.0.1/1218 && echo -e "GET /health HTTP/1.1\r\nhost: 127.0.0.1\r\n\r\n" >&3 && grep ok -s -m 1 <&3' # Kafka consumer responsible for feeding events into Clickhouse snuba-errors-consumer: <<: *snuba_defaults - command: rust-consumer --storage errors --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage errors --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults # Kafka consumer responsible for feeding outcomes into Clickhouse # Use --auto-offset-reset=earliest to recover up to 7 days of TSDB data # since we did not do a proper migration snuba-outcomes-consumer: <<: *snuba_defaults - command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults snuba-outcomes-billing-consumer: <<: *snuba_defaults - command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --raw-events-topic outcomes-billing + command: rust-consumer --storage outcomes_raw --consumer-group snuba-consumers --auto-offset-reset=earliest --max-batch-time-ms 750 --no-strict-offset-reset --raw-events-topic outcomes-billing --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults snuba-group-attributes-consumer: <<: *snuba_defaults - command: rust-consumer --storage group_attributes --consumer-group snuba-group-attributes-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage group_attributes --consumer-group snuba-group-attributes-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults snuba-replacer: <<: *snuba_defaults command: replacer --storage errors --auto-offset-reset=latest --no-strict-offset-reset snuba-subscription-consumer-events: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset events --entity events --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-events-subscriptions-consumers --followed-consumer-group=snuba-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset events --entity events --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-events-subscriptions-consumers --followed-consumer-group=snuba-consumers --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults ############################################# ## Feature Complete Sentry Snuba Consumers ## ############################################# # Kafka consumer responsible for feeding transactions data into Clickhouse snuba-transactions-consumer: <<: *snuba_defaults - command: rust-consumer --storage transactions --consumer-group transactions_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage transactions --consumer-group transactions_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-replays-consumer: <<: *snuba_defaults - command: rust-consumer --storage replays --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage replays --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-issue-occurrence-consumer: <<: *snuba_defaults - command: rust-consumer --storage search_issues --consumer-group generic_events_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage search_issues --consumer-group generic_events_group --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-metrics-consumer: <<: *snuba_defaults - command: rust-consumer --storage metrics_raw --consumer-group snuba-metrics-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage metrics_raw --consumer-group snuba-metrics-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-subscription-consumer-transactions: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset transactions --entity transactions --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-transactions-subscriptions-consumers --followed-consumer-group=transactions_group --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset transactions --entity transactions --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-transactions-subscriptions-consumers --followed-consumer-group=transactions_group --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-subscription-consumer-metrics: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset metrics --entity metrics_sets --entity metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-metrics-subscriptions-consumers --followed-consumer-group=snuba-metrics-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset metrics --entity metrics_sets --entity metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-metrics-subscriptions-consumers --followed-consumer-group=snuba-metrics-consumers --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-subscription-consumer-generic-metrics-distributions: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_distributions --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-distributions-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-distributions-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_distributions --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-distributions-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-distributions-consumers --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-subscription-consumer-generic-metrics-sets: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_sets --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-sets-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-sets-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_sets --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-sets-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-sets-consumers --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-subscription-consumer-generic-metrics-counters: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-counters-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-counters-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_counters --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-counters-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-counters-consumers --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-subscription-consumer-generic-metrics-gauges: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_gauges --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-gauges-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-gauges-consumers --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset generic_metrics --entity=generic_metrics_gauges --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-generic-metrics-gauges-subscriptions-schedulers --followed-consumer-group=snuba-gen-metrics-gauges-consumers --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-generic-metrics-distributions-consumer: <<: *snuba_defaults - command: rust-consumer --storage generic_metrics_distributions_raw --consumer-group snuba-gen-metrics-distributions-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage generic_metrics_distributions_raw --consumer-group snuba-gen-metrics-distributions-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-generic-metrics-sets-consumer: <<: *snuba_defaults - command: rust-consumer --storage generic_metrics_sets_raw --consumer-group snuba-gen-metrics-sets-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage generic_metrics_sets_raw --consumer-group snuba-gen-metrics-sets-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-generic-metrics-counters-consumer: <<: *snuba_defaults - command: rust-consumer --storage generic_metrics_counters_raw --consumer-group snuba-gen-metrics-counters-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage generic_metrics_counters_raw --consumer-group snuba-gen-metrics-counters-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-generic-metrics-gauges-consumer: <<: *snuba_defaults - command: rust-consumer --storage generic_metrics_gauges_raw --consumer-group snuba-gen-metrics-gauges-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage generic_metrics_gauges_raw --consumer-group snuba-gen-metrics-gauges-consumers --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-profiling-profiles-consumer: <<: *snuba_defaults - command: rust-consumer --storage profiles --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + command: rust-consumer --storage profiles --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-profiling-functions-consumer: <<: *snuba_defaults - command: rust-consumer --storage functions_raw --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + command: rust-consumer --storage functions_raw --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-profiling-profile-chunks-consumer: <<: *snuba_defaults - command: rust-consumer --storage profile_chunks --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + command: rust-consumer --storage profile_chunks --consumer-group snuba-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-spans-consumer: <<: *snuba_defaults - command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset + command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-eap-items-consumer: <<: *snuba_defaults - command: rust-consumer --storage eap_items --consumer-group eap_items_group --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --use-rust-processor + command: rust-consumer --storage eap_items --consumer-group eap_items_group --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --use-rust-processor --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete snuba-subscription-consumer-eap-items: <<: *snuba_defaults - command: subscriptions-scheduler-executor --dataset events_analytics_platform --entity eap_items --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-eap-items-subscriptions-consumers --followed-consumer-group=eap_items_group --schedule-ttl=60 --stale-threshold-seconds=900 + command: subscriptions-scheduler-executor --dataset events_analytics_platform --entity eap_items --auto-offset-reset=latest --no-strict-offset-reset --consumer-group=snuba-eap-items-subscriptions-consumers --followed-consumer-group=eap_items_group --schedule-ttl=60 --stale-threshold-seconds=900 --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults snuba-uptime-results-consumer: <<: *snuba_defaults - command: rust-consumer --storage uptime_monitor_checks --consumer-group snuba-uptime-results --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset + command: rust-consumer --storage uptime_monitor_checks --consumer-group snuba-uptime-results --auto-offset-reset=latest --max-batch-time-ms 750 --no-strict-offset-reset --health-check-file /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete symbolicator: @@ -349,6 +415,14 @@ services: read_only: true source: ./symbolicator target: /etc/symbolicator + healthcheck: + <<: *healthcheck_defaults + test: + - "CMD" + - "/bin/bash" + - "-c" + # Courtesy of https://unix.stackexchange.com/a/234089/108960 + - 'exec 3<>/dev/tcp/127.0.0.1/3021 && echo -e "GET /healthcheck HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n" >&3 && grep OK -s -m 1 <&3' command: run -c /etc/symbolicator/config.yml symbolicator-cleanup: <<: [*restart_policy, *pull_policy] @@ -380,119 +454,175 @@ services: worker: <<: *sentry_defaults command: run worker + healthcheck: + <<: *healthcheck_defaults + test: + - CMD + - sentry + - exec + - -c + - 'from sentry.celery import app; import os; dest="celery@{}".format(os.environ["HOSTNAME"]); print(app.control.ping(destination=[dest], timeout=5)[0][dest]["ok"])' events-consumer: <<: *sentry_defaults - command: run consumer ingest-events --consumer-group ingest-consumer + command: run consumer ingest-events --consumer-group ingest-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults attachments-consumer: <<: *sentry_defaults - command: run consumer ingest-attachments --consumer-group ingest-consumer + command: run consumer ingest-attachments --consumer-group ingest-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults post-process-forwarder-errors: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset post-process-forwarder-errors --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-commit-log --synchronize-commit-group=snuba-consumers + command: run consumer --no-strict-offset-reset post-process-forwarder-errors --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-commit-log --synchronize-commit-group=snuba-consumers --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults subscription-consumer-events: <<: *sentry_defaults - command: run consumer events-subscription-results --consumer-group query-subscription-consumer + command: run consumer events-subscription-results --consumer-group query-subscription-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults ############################################## ## Feature Complete Sentry Ingest Consumers ## ############################################## transactions-consumer: <<: *sentry_defaults - command: run consumer ingest-transactions --consumer-group ingest-consumer + command: run consumer ingest-transactions --consumer-group ingest-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete metrics-consumer: <<: *sentry_defaults - command: run consumer ingest-metrics --consumer-group metrics-consumer + command: run consumer ingest-metrics --consumer-group metrics-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete generic-metrics-consumer: <<: *sentry_defaults - command: run consumer ingest-generic-metrics --consumer-group generic-metrics-consumer + command: run consumer ingest-generic-metrics --consumer-group generic-metrics-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete billing-metrics-consumer: <<: *sentry_defaults - command: run consumer billing-metrics-consumer --consumer-group billing-metrics-consumer + command: run consumer billing-metrics-consumer --consumer-group billing-metrics-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete ingest-replay-recordings: <<: *sentry_defaults - command: run consumer ingest-replay-recordings --consumer-group ingest-replay-recordings + command: run consumer ingest-replay-recordings --consumer-group ingest-replay-recordings --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete ingest-occurrences: <<: *sentry_defaults - command: run consumer ingest-occurrences --consumer-group ingest-occurrences + command: run consumer ingest-occurrences --consumer-group ingest-occurrences --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete ingest-profiles: <<: *sentry_defaults - command: run consumer ingest-profiles --consumer-group ingest-profiles + command: run consumer ingest-profiles --consumer-group ingest-profiles --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete ingest-monitors: <<: *sentry_defaults - command: run consumer ingest-monitors --consumer-group ingest-monitors + command: run consumer ingest-monitors --consumer-group ingest-monitors --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete ingest-feedback-events: <<: *sentry_defaults - command: run consumer ingest-feedback-events --consumer-group ingest-feedback + command: run consumer ingest-feedback-events --consumer-group ingest-feedback --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete process-spans: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset process-spans --consumer-group process-spans + command: run consumer --no-strict-offset-reset process-spans --consumer-group process-spans --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete process-segments: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset process-segments --consumer-group process-segments + command: run consumer --no-strict-offset-reset process-segments --consumer-group process-segments --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete monitors-clock-tick: <<: *sentry_defaults - command: run consumer monitors-clock-tick --consumer-group monitors-clock-tick + command: run consumer monitors-clock-tick --consumer-group monitors-clock-tick --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete monitors-clock-tasks: <<: *sentry_defaults - command: run consumer monitors-clock-tasks --consumer-group monitors-clock-tasks + command: run consumer monitors-clock-tasks --consumer-group monitors-clock-tasks --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete uptime-results: <<: *sentry_defaults - command: run consumer uptime-results --consumer-group uptime-results + command: run consumer uptime-results --consumer-group uptime-results --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete post-process-forwarder-transactions: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset post-process-forwarder-transactions --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-transactions-commit-log --synchronize-commit-group transactions_group + command: run consumer --no-strict-offset-reset post-process-forwarder-transactions --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-transactions-commit-log --synchronize-commit-group transactions_group --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete post-process-forwarder-issue-platform: <<: *sentry_defaults - command: run consumer --no-strict-offset-reset post-process-forwarder-issue-platform --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-generic-events-commit-log --synchronize-commit-group generic_events_group + command: run consumer --no-strict-offset-reset post-process-forwarder-issue-platform --consumer-group post-process-forwarder --synchronize-commit-log-topic=snuba-generic-events-commit-log --synchronize-commit-group generic_events_group --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete subscription-consumer-transactions: <<: *sentry_defaults - command: run consumer transactions-subscription-results --consumer-group query-subscription-consumer + command: run consumer transactions-subscription-results --consumer-group query-subscription-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete subscription-consumer-eap-items: <<: *sentry_defaults - command: run consumer subscription-results-eap-items --consumer-group subscription-results-eap-items + command: run consumer subscription-results-eap-items --consumer-group subscription-results-eap-items --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete subscription-consumer-metrics: <<: *sentry_defaults - command: run consumer metrics-subscription-results --consumer-group query-subscription-consumer + command: run consumer metrics-subscription-results --consumer-group query-subscription-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete subscription-consumer-generic-metrics: <<: *sentry_defaults - command: run consumer generic-metrics-subscription-results --consumer-group query-subscription-consumer + command: run consumer generic-metrics-subscription-results --consumer-group query-subscription-consumer --healthcheck-file-path /tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults profiles: - feature-complete sentry-cleanup: @@ -516,6 +646,12 @@ services: target: /etc/nginx/nginx.conf - sentry-nginx-cache:/var/cache/nginx - sentry-nginx-www:/var/www + healthcheck: + <<: *healthcheck_defaults + test: + - "CMD" + - "/usr/bin/curl" + - http://localhost depends_on: - web - relay @@ -566,6 +702,14 @@ services: SENTRY_SNUBA_HOST: "http://snuba-api:1218/" volumes: - sentry-vroom:/var/vroom/sentry-profiles + healthcheck: + <<: *healthcheck_defaults + test: + - "CMD" + - "/bin/bash" + - "-c" + # Courtesy of https://unix.stackexchange.com/a/234089/108960 + - 'exec 3<>/dev/tcp/127.0.0.1/8085 && echo -e "GET /health HTTP/1.1\r\nhost: 127.0.0.1\r\n\r\n" >&3 && grep OK -s -m 1 <&3' depends_on: kafka: <<: *depends_on-healthy From 4666d443c7779474dfc29ef7cd19e99755c972ce Mon Sep 17 00:00:00 2001 From: mzglinski Date: Thu, 14 Aug 2025 13:29:46 +0200 Subject: [PATCH 238/287] fix: adjust file healthcheck durations (#3874) --- .env | 4 ++-- action.yaml | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.env b/.env index 6570bf8c28f..ba8cf6267b7 100644 --- a/.env +++ b/.env @@ -22,8 +22,8 @@ HEALTHCHECK_RETRIES=10 HEALTHCHECK_START_PERIOD=10s HEALTHCHECK_FILE_INTERVAL=60s HEALTHCHECK_FILE_TIMEOUT=10s -HEALTHCHECK_FILE_RETRIES=1 -HEALTHCHECK_FILE_START_PERIOD=180s +HEALTHCHECK_FILE_RETRIES=3 +HEALTHCHECK_FILE_START_PERIOD=600s # Caution: Raising max connections of postgres increases CPU and RAM usage # see https://github.com/getsentry/self-hosted/pull/2740 for more information POSTGRES_MAX_CONNECTIONS=100 diff --git a/action.yaml b/action.yaml index 40b76283268..99ade7523da 100644 --- a/action.yaml +++ b/action.yaml @@ -142,6 +142,17 @@ runs: volumes: | sentry-kafka + - name: Setup swapfile + shell: bash + if: matrix.os == 'ubuntu-24.04' + run: | + sudo fallocate -l 16G /swapfile + sudo chmod 600 /swapfile + sudo mkswap /swapfile + sudo swapon /swapfile + sudo swapon --show + free -h + - name: Integration Test shell: bash run: | From 0f606d28b86876f91865ac78d52a908e3c623c05 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Thu, 14 Aug 2025 15:11:49 +0330 Subject: [PATCH 239/287] Set minimum bash version to 4.4.0 (#3873) --- install/_min-requirements.sh | 2 ++ install/check-minimum-requirements.sh | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/install/_min-requirements.sh b/install/_min-requirements.sh index 36c5dc0f625..2ffded50700 100644 --- a/install/_min-requirements.sh +++ b/install/_min-requirements.sh @@ -5,6 +5,8 @@ MIN_COMPOSE_VERSION='2.32.2' MIN_PODMAN_VERSION='4.9.3' MIN_PODMAN_COMPOSE_VERSION='1.3.0' +MIN_BASH_VERSION='4.4.0' + # 16 GB minimum host RAM, but there'll be some overhead outside of what # can be allotted to docker if [[ "$COMPOSE_PROFILES" == "errors-only" ]]; then diff --git a/install/check-minimum-requirements.sh b/install/check-minimum-requirements.sh index 322de339759..91e5116b6d3 100644 --- a/install/check-minimum-requirements.sh +++ b/install/check-minimum-requirements.sh @@ -54,4 +54,9 @@ if [[ ! "$SKIP_SSE42_REQUIREMENTS" -eq 1 && "$IS_KVM" -eq 0 && "$DOCKER_ARCH" = fi fi +if ! vergte "${BASH_VERSION}" "${MIN_BASH_VERSION}"; then + echo "FAIL: Expected minimum bash version to be ${MIN_BASH_VERSION} but found ${BASH_VERSION}" + exit 1 +fi + echo "${_endgroup}" From 3614389d887adfe788c51f8935ccda324d4041a1 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 14 Aug 2025 22:09:55 +0700 Subject: [PATCH 240/287] fix: setup swapfile only if runner architecture is X64 or X86 (#3876) Using matrix.os only works on self-hosted. SH e2e test on other repositories would still fail. Refer to https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#runner-context --- action.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 99ade7523da..0a655e53bd5 100644 --- a/action.yaml +++ b/action.yaml @@ -144,7 +144,7 @@ runs: - name: Setup swapfile shell: bash - if: matrix.os == 'ubuntu-24.04' + if: runner.arch == 'X64' || runner.arch == 'X86' run: | sudo fallocate -l 16G /swapfile sudo chmod 600 /swapfile From e4d311bb5db4a8fdf0ba4f57eab5e184a534a170 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Thu, 14 Aug 2025 22:26:18 +0700 Subject: [PATCH 241/287] feat: Relay healthcheck (#3875) --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 437bdc7694c..8873990cb3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -674,6 +674,9 @@ services: <<: *depends_on-healthy web: <<: *depends_on-healthy + healthcheck: + <<: *healthcheck_defaults + test: ["CMD", "/bin/relay", "healthcheck"] taskbroker: <<: *restart_policy image: "$TASKBROKER_IMAGE" From f7acf12d764200b65956c2f2e35d478242f88e0b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 15 Aug 2025 18:07:35 +0000 Subject: [PATCH 242/287] release: 25.8.0 --- .env | 14 +++++++------- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.env b/.env index ba8cf6267b7..87fed26356f 100644 --- a/.env +++ b/.env @@ -9,13 +9,13 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly -SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly -RELAY_IMAGE=ghcr.io/getsentry/relay:nightly -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly -VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly -UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly +SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.8.0 +SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.8.0 +RELAY_IMAGE=ghcr.io/getsentry/relay:25.8.0 +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.8.0 +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.8.0 +VROOM_IMAGE=ghcr.io/getsentry/vroom:25.8.0 +UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:25.8.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index ea0deec5a96..88a8d4528f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 25.8.0 + +### Various fixes & improvements + +- feat: Relay healthcheck (#3875) by @aldy505 +- fix: setup swapfile only if runner architecture is X64 or X86 (#3876) by @aldy505 +- Set minimum bash version to 4.4.0 (#3873) by @aminvakil +- fix: adjust file healthcheck durations (#3874) by @mzglinski +- feat: healthchecks for sentry components (#3859) by @mzglinski +- fix(eap): Fix dataset parameter to target spans (#3866) by @phacops +- build(deps): bump actions/create-github-app-token from 2.0.6 to 2.1.0 (#3865) by @dependabot +- fix(scripts): use `env` to find `bash` interpreter (#3861) by @Zaczero +- fix(scripts): every known flags should be shifted before executing the sentry command (#3831) by @aldy505 +- fix: uptime checker image should be bumped to the tagged release (#3858) by @aldy505 +- fix(enhancement): ensure correct ownership check before setting permissions of profiles (#3855) by @LvckyAPI +- chore(features): cleanup feature flags grouped by its' category (#3843) by @aldy505 +- fix: add schedulers for generic metrics subscriptions (#3847) by @mzglinski +- feat: Continue using celery in self-hosted for now (#3845) by @markstory +- feat(features): add `profiling-view` flag (#3837) by @aldy505 +- Potential fix for code scanning alert no. 12: Workflow does not contain permissions (#3822) by @aldy505 +- docs: clearly state that `system.internal-url-prefix` shouldn't be changed (#3829) by @aldy505 +- feat(install): Adds support for podman(compose) (#3673) by @DuncanConroy +- fix(action): missing project directory path for failure inspection (#3825) by @aldy505 +- Cleanup unused feature flags (#3820) by @doc-sheet +- feat: inspect docker compose failure on self-hosted e2e action (#3817) by @aldy505 + ## 25.7.0 ### Various fixes & improvements From 17e638f8f049b10c4eff32ae8fc51ab0c9d1e7d1 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 15 Aug 2025 20:03:08 +0000 Subject: [PATCH 243/287] build: Set master version to nightly #skip-changelog --- .env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 87fed26356f..ba8cf6267b7 100644 --- a/.env +++ b/.env @@ -9,13 +9,13 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.8.0 -SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.8.0 -RELAY_IMAGE=ghcr.io/getsentry/relay:25.8.0 -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.8.0 -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.8.0 -VROOM_IMAGE=ghcr.io/getsentry/vroom:25.8.0 -UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:25.8.0 +SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly +SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly +RELAY_IMAGE=ghcr.io/getsentry/relay:nightly +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly +VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly +UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From dc5ac0792db9697f525b80604c6547ef8c313ceb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:02:54 +0100 Subject: [PATCH 244/287] build(deps): bump actions/checkout from 4 to 5 (#3883) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/fast-revert.yml | 2 +- .github/workflows/pre-commit.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/shellcheck.yml | 2 +- .github/workflows/test.yml | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/fast-revert.yml b/.github/workflows/fast-revert.yml index d3699da4609..6b88ed831a5 100644 --- a/.github/workflows/fast-revert.yml +++ b/.github/workflows/fast-revert.yml @@ -19,7 +19,7 @@ jobs: if: | github.event_name == 'workflow_dispatch' || github.event.label.name == 'Trigger: Revert' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: ${{ secrets.BUMP_SENTRY_TOKEN }} - uses: getsentry/action-fast-revert@v2.0.1 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index f8169727aff..6a449b9da7c 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -9,7 +9,7 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87699d3b48d..c7d447e130a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: token: ${{ steps.token.outputs.token }} fetch-depth: 0 @@ -46,7 +46,7 @@ jobs: name: Create release on self-hosted dogfood instance needs: release steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - uses: getsentry/action-release@v3 diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index c5cc25e8cbb..889817caaf8 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Repository checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ad0a1c4c8b..99448af00c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: name: ${{ matrix.os == 'ubuntu-24.04-arm' && 'unit tests (arm64)' || 'unit tests' }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Get Compose uses: ./get-compose-action @@ -53,7 +53,7 @@ jobs: CONTAINER_ENGINE_PODMAN: ${{ matrix.container_engine == 'podman' && '1' || '0' }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install Podman if: matrix.container_engine == 'podman' From 7ef1b36b90ada2b8545bd239019e332962a40fea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:05:35 +0100 Subject: [PATCH 245/287] build(deps): bump actions/create-github-app-token from 2.1.0 to 2.1.1 (#3885) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.1.0 to 2.1.1. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/0f859bf9e69e887678d5bbfbee594437cb440ffe...a8d616148505b5069dccd32f177bb87d7f39123b) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7d447e130a..21115c4ce4c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@0f859bf9e69e887678d5bbfbee594437cb440ffe # v2.1.0 + uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From 657a685cbef5e04d39b0033fadb9e9e6177b6b7c Mon Sep 17 00:00:00 2001 From: Iven Schlenther Date: Tue, 19 Aug 2025 20:39:04 +0200 Subject: [PATCH 246/287] fix(enhancement): search for permissions on docker container instead of host and combine it in one command for performance enhancement (#3890) * fix(enhancement): ensure correct ownership check before setting permissions of profiles * fix(enhancement): search for permissions on docker container instead of host and combine it in one command for performance enhancement Resolves #3882 * fix(enhancement): search for permissions on docker container instead of host --- install/ensure-correct-permissions-profiles-dir.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/ensure-correct-permissions-profiles-dir.sh b/install/ensure-correct-permissions-profiles-dir.sh index 5bdadecc70c..92792b26e50 100755 --- a/install/ensure-correct-permissions-profiles-dir.sh +++ b/install/ensure-correct-permissions-profiles-dir.sh @@ -5,8 +5,8 @@ echo "${_group}Ensuring correct permissions on profiles directory ..." # Check if the parent directory of /var/vroom/sentry-profiles is already owned by vroom:vroom -if [ "$(stat -c '%U:%G' /var/vroom)" = "vroom:vroom" ]; then - echo "Ownership of /var/vroom is already set to vroom:vroom. Skipping chown." +if [ "$($dcr --no-deps --entrypoint /bin/bash --user root vroom -c "stat -c '%U:%G' /var/vroom/sentry-profiles" 2>/dev/null)" = "vroom:vroom" ]; then + echo "Ownership of /var/vroom/sentry-profiles is already set to vroom:vroom. Skipping chown." else $dcr --no-deps --entrypoint /bin/bash --user root vroom -c 'chown -R vroom:vroom /var/vroom/sentry-profiles && chmod -R o+rwx /var/vroom/sentry-profiles' fi From 8641076786ab4650025daf0bb14d5b0656c08c56 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 20 Aug 2025 01:39:30 +0700 Subject: [PATCH 247/287] chore: resolve GHA code scanning alerts (#3889) Resolves https://github.com/getsentry/self-hosted/security/code-scanning/2 Resolves https://github.com/getsentry/self-hosted/security/code-scanning/4 Resolves https://github.com/getsentry/self-hosted/security/code-scanning/7 Resolves https://github.com/getsentry/self-hosted/security/code-scanning/14 Resolves https://github.com/getsentry/self-hosted/security/code-scanning/15 --- .github/workflows/enforce-license-compliance.yml | 3 +++ .github/workflows/pre-commit.yml | 3 +++ .github/workflows/release.yml | 2 ++ .github/workflows/shellcheck.yml | 4 +++- .github/workflows/test.yml | 6 ++++-- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml index 02ca9d8e3f6..43ce1996390 100644 --- a/.github/workflows/enforce-license-compliance.yml +++ b/.github/workflows/enforce-license-compliance.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [master] +permissions: + contents: read + jobs: enforce-license-compliance: if: github.repository_owner == 'getsentry' diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 6a449b9da7c..1ba1d915924 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -5,6 +5,9 @@ on: push: branches: [master] +permissions: + contents: read + jobs: pre-commit: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21115c4ce4c..bbd7dfd659f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,8 @@ on: # We also make this an hour after all others such as Sentry, # Snuba, and Relay to make sure their releases finish. - cron: "0 18 15 * *" +permissions: + contents: read jobs: release: if: github.repository_owner == 'getsentry' diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 889817caaf8..692da9f4a8f 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -1,4 +1,3 @@ ---- name: "ShellCheck" on: push: @@ -10,6 +9,9 @@ on: - "**.sh" branches: [master] +permissions: + contents: read + jobs: shellcheck: name: ShellCheck diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99448af00c3..c2a431f7bae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,5 +1,4 @@ -permissions: - contents: read + name: Test on: # Run CI on all pushes to the master and release/** branches, and on all new @@ -17,6 +16,9 @@ concurrency: group: ${{ github.ref_name || github.sha }} cancel-in-progress: true +permissions: + contents: read + defaults: run: shell: bash From 2862432828a35b106dce38682b2ed6fb41c53d11 Mon Sep 17 00:00:00 2001 From: Frederik Spang Date: Sat, 23 Aug 2025 15:18:13 +0200 Subject: [PATCH 248/287] Add pgbouncer (#3884) * Add patch for pgbouncer * pgcat over pgbouncer * Add patch for .env file * Apply patches and add initial pgcat tolm file * feat: hardcode pgcat image * Fixes from review * Align usernames defaults * Remove postgres from default depends_on; Covered by pgcat by extension * Set user and password - pgcat maybe doesnt support host auth trust * Pool name maybe has to match, for some reason * Use healthcheck from pgcat PR * Reduce pool size, leave some for healthchecks and other clients running * Start pgcat for bash scripts with postgres * Update docker-compose.yml * Use pgbouncer * Revert to TRUST method --- docker-compose.yml | 20 +++++++++++++++++++- install/set-up-and-migrate-database.sh | 1 + sentry-admin.sh | 1 + sentry/sentry.conf.example.py | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8873990cb3e..77a4a938e48 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ x-sentry-defaults: &sentry_defaults <<: *depends_on-healthy kafka: <<: *depends_on-healthy - postgres: + pgbouncer: <<: *depends_on-healthy memcached: <<: *depends_on-default @@ -144,6 +144,24 @@ services: POSTGRES_HOST_AUTH_METHOD: "trust" volumes: - "sentry-postgres:/var/lib/postgresql/data" + pgbouncer: + image: "edoburu/pgbouncer:v1.24.1-p1" + healthcheck: + <<: *healthcheck_defaults + # Using default user "postgres" from sentry/sentry.conf.example.py or value of POSTGRES_USER if provided + test: ["CMD-SHELL", "psql -U postgres -p 5432 -h 127.0.0.1 -tA -c \"select 1;\" -d postgres >/dev/null"] + depends_on: + postgres: + <<: *depends_on-healthy + environment: + DB_USER: ${POSTGRES_USER:-postgres} + DB_HOST: postgres + DB_NAME: postgres + AUTH_TYPE: trust + POOL_MODE: transaction + ADMIN_USERS: postgres,sentry + MAX_CLIENT_CONN: 10000 + kafka: <<: *restart_policy image: "confluentinc/cp-kafka:7.6.1" diff --git a/install/set-up-and-migrate-database.sh b/install/set-up-and-migrate-database.sh index 5ddf324c0b8..debebd2531d 100644 --- a/install/set-up-and-migrate-database.sh +++ b/install/set-up-and-migrate-database.sh @@ -3,6 +3,7 @@ echo "${_group}Setting up / migrating database ..." if [[ -z "${SKIP_SENTRY_MIGRATIONS:-}" ]]; then # Fixes https://github.com/getsentry/self-hosted/issues/2758, where a migration fails due to indexing issue start_service_and_wait_ready postgres + start_service_and_wait_ready pgbouncer os=$($dc exec postgres cat /etc/os-release | grep 'ID=debian') if [[ -z $os ]]; then diff --git a/sentry-admin.sh b/sentry-admin.sh index aeea2b36845..ef5608488f9 100755 --- a/sentry-admin.sh +++ b/sentry-admin.sh @@ -24,6 +24,7 @@ on the host filesystem. Commands that write files should write them to the '/sen # Actual invocation that runs the command in the container. invocation() { start_service_and_wait_ready postgres + start_service_and_wait_ready pgbouncer start_service_and_wait_ready redis --wait $dcr --no-deps -v "$VOLUME_MAPPING" -T -e SENTRY_LOG_LEVEL=CRITICAL web "$@" 2>&1 } diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 826f43ae2d6..944dc6399b3 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -48,7 +48,7 @@ def get_internal_network(): "NAME": "postgres", "USER": "postgres", "PASSWORD": "", - "HOST": "postgres", + "HOST": "pgbouncer", "PORT": "", } } From b7bb06470e150410d74ba41fa68c69ac884ee3df Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Sat, 23 Aug 2025 17:06:20 +0330 Subject: [PATCH 249/287] Revert "increase postgres max_connections above 100 connections (#2740)" (#3899) This reverts commit 7691addcb631fee42389896cf4eead0794f11a9d. --- .env | 3 --- docker-compose.yml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.env b/.env index ba8cf6267b7..dfe279267f8 100644 --- a/.env +++ b/.env @@ -24,8 +24,5 @@ HEALTHCHECK_FILE_INTERVAL=60s HEALTHCHECK_FILE_TIMEOUT=10s HEALTHCHECK_FILE_RETRIES=3 HEALTHCHECK_FILE_START_PERIOD=600s -# Caution: Raising max connections of postgres increases CPU and RAM usage -# see https://github.com/getsentry/self-hosted/pull/2740 for more information -POSTGRES_MAX_CONNECTIONS=100 # Set SETUP_JS_SDK_ASSETS to 1 to enable the setup of JS SDK assets # SETUP_JS_SDK_ASSETS=1 diff --git a/docker-compose.yml b/docker-compose.yml index 77a4a938e48..7d89d34c394 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -139,7 +139,7 @@ services: # Using default user "postgres" from sentry/sentry.conf.example.py or value of POSTGRES_USER if provided test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] command: - ["postgres", "-c", "max_connections=${POSTGRES_MAX_CONNECTIONS:-100}"] + ["postgres"] environment: POSTGRES_HOST_AUTH_METHOD: "trust" volumes: From 67c32e883dae0021f5be7e31fdbcefc3cff6c880 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sat, 23 Aug 2025 21:15:07 +0700 Subject: [PATCH 250/287] chore(deps): bump patches version (#3879) Except nginx, they don't seem to have any breaking change. See https://nginx.org/en/CHANGES Other than nginx, patches version are available. --- docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7d89d34c394..2290a301429 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,7 +115,7 @@ services: test: echo stats | nc 127.0.0.1 11211 redis: <<: *restart_policy - image: "redis:6.2.14-alpine" + image: "redis:6.2.19-alpine" healthcheck: <<: *healthcheck_defaults test: redis-cli ping | grep PONG @@ -133,7 +133,7 @@ services: postgres: <<: *restart_policy # Using the same postgres version as Sentry dev for consistency purposes - image: "postgres:14.11" + image: "postgres:14.19-bookworm" healthcheck: <<: *healthcheck_defaults # Using default user "postgres" from sentry/sentry.conf.example.py or value of POSTGRES_USER if provided @@ -164,7 +164,7 @@ services: kafka: <<: *restart_policy - image: "confluentinc/cp-kafka:7.6.1" + image: "confluentinc/cp-kafka:7.6.6" environment: # https://docs.confluent.io/platform/current/installation/docker/config-reference.html#cp-kakfa-example KAFKA_PROCESS_ROLES: "broker,controller" @@ -656,7 +656,7 @@ services: <<: *restart_policy ports: - "$SENTRY_BIND:80/tcp" - image: "nginx:1.25.4-alpine" + image: "nginx:1.29.1-alpine" volumes: - type: bind read_only: true From de2139890d3d6b28682f2a07855e9aefc0b030ae Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 2 Sep 2025 21:35:33 +0700 Subject: [PATCH 251/287] fix: ensuring vroom permission should be skipped on errors-only (#3911) * fix: ensuring vroom permission should be skipped on errors-only * feat: enable swap on all runners * feat: don't exit on install script --- action.yaml | 2 +- ...ensure-correct-permissions-profiles-dir.sh | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/action.yaml b/action.yaml index 0a655e53bd5..5c291eede3e 100644 --- a/action.yaml +++ b/action.yaml @@ -144,8 +144,8 @@ runs: - name: Setup swapfile shell: bash - if: runner.arch == 'X64' || runner.arch == 'X86' run: | + sudo swapoff -a sudo fallocate -l 16G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile diff --git a/install/ensure-correct-permissions-profiles-dir.sh b/install/ensure-correct-permissions-profiles-dir.sh index 92792b26e50..2fd95b885e6 100755 --- a/install/ensure-correct-permissions-profiles-dir.sh +++ b/install/ensure-correct-permissions-profiles-dir.sh @@ -2,13 +2,16 @@ # TODO: Remove this after the next hard-stop -echo "${_group}Ensuring correct permissions on profiles directory ..." +# Should only run when `$COMPOSE_PROFILES` is set to `feature-complete` +if [[ "$COMPOSE_PROFILES" == "feature-complete" ]]; then + echo "${_group}Ensuring correct permissions on profiles directory ..." -# Check if the parent directory of /var/vroom/sentry-profiles is already owned by vroom:vroom -if [ "$($dcr --no-deps --entrypoint /bin/bash --user root vroom -c "stat -c '%U:%G' /var/vroom/sentry-profiles" 2>/dev/null)" = "vroom:vroom" ]; then - echo "Ownership of /var/vroom/sentry-profiles is already set to vroom:vroom. Skipping chown." -else - $dcr --no-deps --entrypoint /bin/bash --user root vroom -c 'chown -R vroom:vroom /var/vroom/sentry-profiles && chmod -R o+rwx /var/vroom/sentry-profiles' -fi + # Check if the parent directory of /var/vroom/sentry-profiles is already owned by vroom:vroom + if [ "$($dcr --no-deps --entrypoint /bin/bash --user root vroom -c "stat -c '%U:%G' /var/vroom/sentry-profiles" 2>/dev/null)" = "vroom:vroom" ]; then + echo "Ownership of /var/vroom/sentry-profiles is already set to vroom:vroom. Skipping chown." + else + $dcr --no-deps --entrypoint /bin/bash --user root vroom -c 'chown -R vroom:vroom /var/vroom/sentry-profiles && chmod -R o+rwx /var/vroom/sentry-profiles' + fi -echo "${_endgroup}" + echo "${_endgroup}" +fi From f5773e8ed525f527feda8a16d952b9bfa1fcc9f0 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 3 Sep 2025 00:21:03 +0700 Subject: [PATCH 252/287] feat: enable Logs feature (#3912) * feat: enable Logs feature * feat: enable swap on all runners See https://github.com/getsentry/self-hosted/pull/3911/commits/ce819b8d99eac7f20c786492fe3c5b434c14e753 --- _integration-test/test_01_basics.py | 29 +++++++++++++++++++++++++++++ sentry/sentry.conf.example.py | 7 +++++++ 2 files changed, 36 insertions(+) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index b759f629730..caa7c6ff519 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -11,6 +11,7 @@ import httpx import pytest import sentry_sdk +from sentry_sdk import logger as sentry_logger from bs4 import BeautifulSoup from cryptography import x509 from cryptography.hazmat.backends import default_backend @@ -406,6 +407,34 @@ def placeholder_fn(): ) +def test_receive_logs_events(client_login): + client, _ = client_login + sentry_sdk.init( + dsn=get_sentry_dsn(client), profiles_sample_rate=1.0, traces_sample_rate=1.0, enable_logs=True, + ) + + sentry_logger.trace('Starting database connection {database}', database="users") + sentry_logger.debug('Cache miss for user {user_id}', user_id=123) + sentry_logger.info('Updated global cache') + sentry_logger.warning('Rate limit reached for endpoint {endpoint}', endpoint='/api/results/') + sentry_logger.error('Failed to process payment. Order: {order_id}. Amount: {amount}', order_id="or_2342", amount=99.99) + sentry_logger.fatal('Database {database} connection pool exhausted', database="users") + sentry_logger.error( + 'Payment processing failed', + attributes={ + 'payment.provider': 'stripe', + 'payment.method': 'credit_card', + 'payment.currency': 'USD', + 'user.subscription_tier': 'premium' + } + ) + + poll_for_response( + f"{SENTRY_TEST_HOST}/api/0/organizations/sentry/events/?dataset=ourlogs&field=sentry.item_id&field=project.id&field=trace&field=severity_number&field=severity&field=timestamp&field=timestamp_precise&field=observed_timestamp&field=message&project=1&statsPeriod=1h", + client, + lambda x: len(json.loads(x)["data"]) > 0, + ) + def test_customizations(): commands = [ [ diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 944dc6399b3..a96e5e6f2ab 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -350,6 +350,13 @@ def get_internal_network(): "organizations:uptime", "organizations:uptime-create-issues", ) + # Logs related flags + + ( + "organizations:ourlogs-enabled", + "organizations:ourlogs-ingestion", + "organizations:ourlogs-stats", + "organizations:ourlogs-replay-ui", + ) } ) From af32d373b253cc79661f118cb857283a523acbca Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 3 Sep 2025 06:05:57 +0700 Subject: [PATCH 253/287] test: run errors-only integration tests (#3910) --- .github/workflows/test.yml | 4 +++- _integration-test/test_01_basics.py | 1 + action.yaml | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2a431f7bae..49298277c4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,8 @@ jobs: matrix: os: [ubuntu-24.04, ubuntu-24.04-arm] container_engine: ['docker'] # TODO: add 'podman' into the list - name: ${{ matrix.os == 'ubuntu-24.04-arm' && (matrix.container_engine == 'docker' && 'integration test (arm64)' || 'integration test (arm64 podman)') || (matrix.container_engine == 'docker' && 'integration test' || 'integration test (podman)') }} + compose_profiles: ['feature-complete', 'errors-only'] + name: ${{ format('integration test{0}{1}{2}', matrix.os == 'ubuntu-24.04-arm' && ' (arm64)' || '', matrix.container_engine == 'podman' && ' (podman)' || '', matrix.compose_profiles == 'errors-only' && ' (errors-only)' || '') }} env: REPORT_SELF_HOSTED_ISSUES: 0 SELF_HOSTED_TESTING_DSN: ${{ vars.SELF_HOSTED_TESTING_DSN }} @@ -71,4 +72,5 @@ jobs: - name: Use action from local checkout uses: './' with: + compose_profiles: ${{ matrix.compose_profiles }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index caa7c6ff519..bc9eb55d48d 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -380,6 +380,7 @@ def test_custom_certificate_authorities(): del os.environ["COMPOSE_FILE"] +@pytest.mark.skipif(os.environ.get("COMPOSE_PROFILES") != "feature-complete", reason="Only run if feature-complete") def test_receive_transaction_events(client_login): client, _ = client_login sentry_sdk.init( diff --git a/action.yaml b/action.yaml index 5c291eede3e..563e06d40d6 100644 --- a/action.yaml +++ b/action.yaml @@ -6,6 +6,9 @@ inputs: image_url: required: false description: "The URL to the built relay, snuba, sentry image to test against." + compose_profiles: + required: false + description: "Docker Compose profile to use. Defaults to feature-complete." CODECOV_TOKEN: required: false description: "The Codecov token to upload coverage." @@ -18,6 +21,7 @@ runs: env: PROJECT_NAME: ${{ inputs.project_name }} IMAGE_URL: ${{ inputs.image_url }} + COMPOSE_PROFILES: ${{ inputs.compose_profiles }} run: | if [[ -n "$PROJECT_NAME" && -n "$IMAGE_URL" ]]; then image_var=$(echo "${PROJECT_NAME}_IMAGE" | tr '[:lower:]' '[:upper:]') @@ -30,6 +34,14 @@ runs: exit 1 fi + # `COMPOSE_PROFILES` may only be `feature-complete` or `errors-only` + if [[ "$COMPOSE_PROFILES" != "" && "$COMPOSE_PROFILES" != "feature-complete" && "$COMPOSE_PROFILES" != "errors-only" ]]; then + echo "COMPOSE_PROFILES must be either unset, or set to either 'feature-complete' or 'errors-only'." + exit 1 + else + echo "COMPOSE_PROFILES=$COMPOSE_PROFILES" >> ${{ github.action_path }}/.env + fi + - name: Setup dev environment shell: bash run: | @@ -155,6 +167,8 @@ runs: - name: Integration Test shell: bash + env: + COMPOSE_PROFILES: ${{ inputs.compose_profiles }} run: | sudo chown root /usr/bin/rsync && sudo chmod u+s /usr/bin/rsync rsync -aW --super --numeric-ids --no-compress --mkpath \ From 85365f1149f9cc61e5cd04e8d81783ceb62cf782 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Wed, 3 Sep 2025 02:36:43 +0330 Subject: [PATCH 254/287] Improve nginx depends_on policy (#3914) --- docker-compose.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2290a301429..a7a276a6a40 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -671,8 +671,12 @@ services: - "/usr/bin/curl" - http://localhost depends_on: - - web - - relay + web: + <<: *depends_on-healthy + restart: true + relay: + <<: *depends_on-healthy + restart: true relay: <<: *restart_policy image: "$RELAY_IMAGE" From e3e7789853fc7316dba98415282e066e504c7825 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 3 Sep 2025 19:25:21 +0700 Subject: [PATCH 255/287] fix(tests): skip logs event test for errors-only (#3915) --- _integration-test/test_01_basics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/_integration-test/test_01_basics.py b/_integration-test/test_01_basics.py index bc9eb55d48d..a552988baa1 100644 --- a/_integration-test/test_01_basics.py +++ b/_integration-test/test_01_basics.py @@ -408,6 +408,7 @@ def placeholder_fn(): ) +@pytest.mark.skipif(os.environ.get("COMPOSE_PROFILES") != "feature-complete", reason="Only run if feature-complete") def test_receive_logs_events(client_login): client, _ = client_login sentry_sdk.init( From 53004dcbc0af9251157fa2cb60d94b0a2552ed2d Mon Sep 17 00:00:00 2001 From: Frederik Spang Date: Sat, 6 Sep 2025 07:32:00 +0200 Subject: [PATCH 256/287] Add restart policy to pgbouncer service (#3925) --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index a7a276a6a40..54a161f375d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -145,6 +145,7 @@ services: volumes: - "sentry-postgres:/var/lib/postgresql/data" pgbouncer: + <<: *restart_policy image: "edoburu/pgbouncer:v1.24.1-p1" healthcheck: <<: *healthcheck_defaults From 2be8c79d64ea937178cb739a983f20ac368af3fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:41:05 +0700 Subject: [PATCH 257/287] build(deps): bump actions/setup-python from 5 to 6 (#3927) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pre-commit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 1ba1d915924..35e1183b6e3 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: 3.x - uses: pre-commit/action@v3.0.1 From 8ad99169994e6c67f50488e27646d30c9ca80703 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 12 Sep 2025 05:08:06 +0700 Subject: [PATCH 258/287] feat: query against `eap` dataset instead of `metrics` dataset for spans (#3923) --- sentry/sentry.conf.example.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index a96e5e6f2ab..3bcd1ff3a7a 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -316,12 +316,15 @@ def get_internal_network(): # Performance/Tracing/Spans related flags + ( "organizations:performance-view", + "organizations:span-stats", "organizations:visibility-explore-view", + "organizations:visibility-explore-range-high", "organizations:transaction-metrics-extraction", "organizations:indexed-spans-extraction", "organizations:insights-entry-points", "organizations:insights-initial-modules", "organizations:insights-addon-modules", + "organizations:insights-modules-use-eap", "organizations:standalone-span-ingestion", "organizations:starfish-mobile-appstart", "projects:span-metrics-extraction", From 7164e22494cb64606908eb502762de6870ee2181 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 12 Sep 2025 07:17:53 +0700 Subject: [PATCH 259/287] feat: enable `issue-views` flag (#3922) --- sentry/sentry.conf.example.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 3bcd1ff3a7a..9c58f55752f 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -293,6 +293,7 @@ def get_internal_network(): for feature in ( "organizations:discover", "organizations:global-views", + "organizations:issue-views", "organizations:incidents", "organizations:integrations-issue-basic", "organizations:integrations-issue-sync", From ce452944917d0848563fbefecd00661a7ef02e1c Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 12 Sep 2025 07:39:27 +0700 Subject: [PATCH 260/287] chore(deps): bump clickhouse to 25.3 (#3878) * chore(deps): bump clickhouse to 25.3 * fix: wrong volume path for clickhouse default password: --- clickhouse/default-password.xml | 10 ++++++++++ docker-compose.yml | 12 ++++++++---- install/upgrade-clickhouse.sh | 34 ++++++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 clickhouse/default-password.xml diff --git a/clickhouse/default-password.xml b/clickhouse/default-password.xml new file mode 100644 index 00000000000..13588039cba --- /dev/null +++ b/clickhouse/default-password.xml @@ -0,0 +1,10 @@ + + + + + + ::/0 + + + + diff --git a/docker-compose.yml b/docker-compose.yml index 54a161f375d..6d59d10700b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -206,7 +206,7 @@ services: build: context: ./clickhouse args: - BASE_IMAGE: "altinity/clickhouse-server:23.8.11.29.altinitystable" + BASE_IMAGE: "altinity/clickhouse-server:25.3.6.10034.altinitystable" ulimits: nofile: soft: 262144 @@ -214,10 +214,14 @@ services: volumes: - "sentry-clickhouse:/var/lib/clickhouse" - "sentry-clickhouse-log:/var/log/clickhouse-server" - - type: bind + - type: "bind" + read_only: true + source: "./clickhouse/config.xml" + target: "/etc/clickhouse-server/config.d/sentry.xml" + - type: "bind" read_only: true - source: ./clickhouse/config.xml - target: /etc/clickhouse-server/config.d/sentry.xml + source: "./clickhouse/default-password.xml" + target: "/etc/clickhouse-server/users.d/default-password.xml" environment: # This limits Clickhouse's memory to 30% of the host memory # If you have high volume and your search return incomplete results diff --git a/install/upgrade-clickhouse.sh b/install/upgrade-clickhouse.sh index 93456b8ddbe..7ac7e826e61 100644 --- a/install/upgrade-clickhouse.sh +++ b/install/upgrade-clickhouse.sh @@ -14,15 +14,47 @@ if $ps_command | grep -q clickhouse; then # Start clickhouse if it is not already running start_service_and_wait_ready clickhouse - # In order to get to 23.8, we need to first upgrade go from 21.8 -> 22.8 -> 23.3 -> 23.8 + # In order to get to 25.3, we need to first upgrade go from 21.8 -> 22.8 -> 23.3 -> 23.8 -> 24.8 -> 25.3 version=$($dc exec clickhouse clickhouse-client -q 'SELECT version()') if [[ "$version" == "21.8.13.1.altinitystable" || "$version" == "21.8.12.29.altinitydev.arm" ]]; then + echo "Detected clickhouse version $version" $dc down clickhouse + + echo "Upgrading clickhouse to 22.8" $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:22.8.15.25.altinitystable clickhouse start_service_and_wait_ready clickhouse $dc down clickhouse + + echo "Upgrading clickhouse to 23.3" $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:23.3.19.33.altinitystable clickhouse start_service_and_wait_ready clickhouse + $dc down clickhouse + + echo "Upgrading clickhouse to 23.8" + $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:23.8.11.29.altinitystable clickhouse + start_service_and_wait_ready clickhouse + $dc down clickhouse + + echo "Upgrading clickhouse to 24.8" + $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:24.8.14.10459.altinitystable clickhouse + start_service_and_wait_ready clickhouse + $dc down clickhouse + + echo "Upgrading clickhouse to 25.3" + $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:25.3.6.10034.altinitystable clickhouse + start_service_and_wait_ready clickhouse + elif [[ "$version" == "23.8.11.29.altinitystable" ]]; then + echo "Detected clickhouse version $version" + $dc down clickhouse + + echo "Upgrading clickhouse to 24.8" + $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:24.8.14.10459.altinitystable clickhouse + start_service_and_wait_ready clickhouse + $dc down clickhouse + + echo "Upgrading clickhouse to 25.3" + $dcb $build_arg BASE_IMAGE=altinity/clickhouse-server:25.3.6.10034.altinitystable clickhouse + start_service_and_wait_ready clickhouse else echo "Detected clickhouse version $version. Skipping upgrades!" fi From 2e7a3ff7ad9435848b5052d1aa91ae42695e76d8 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 12 Sep 2025 21:02:59 +0700 Subject: [PATCH 261/287] feat: install script to migrate sentry.conf.py config to use pgbouncer (#3898) --- _unit-test/migrate-pgbouncer-test.sh | 197 +++++++++++++++++++++++++++ install/migrate-pgbouncer.sh | 82 +++++++++++ install/parse-cli.sh | 8 ++ 3 files changed, 287 insertions(+) create mode 100755 _unit-test/migrate-pgbouncer-test.sh create mode 100644 install/migrate-pgbouncer.sh diff --git a/_unit-test/migrate-pgbouncer-test.sh b/_unit-test/migrate-pgbouncer-test.sh new file mode 100755 index 00000000000..b78ee633c04 --- /dev/null +++ b/_unit-test/migrate-pgbouncer-test.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash + +source _unit-test/_test_setup.sh +source install/dc-detect-version.sh + +source install/ensure-files-from-examples.sh +cp $SENTRY_CONFIG_PY /tmp/sentry_conf_py +# Set the flag to apply automatic updates +export APPLY_AUTOMATIC_CONFIG_UPDATES=1 + +# Declare expected content +expected_db_config=$( + cat <<'EOF' +DATABASES = { + "default": { + "ENGINE": "sentry.db.postgres", + "NAME": "postgres", + "USER": "postgres", + "PASSWORD": "", + "HOST": "pgbouncer", + "PORT": "", + } +} +EOF +) + +echo "Test 1 (pre 25.9.0 release)" +# Modify the `DATABASES = {` to the next `}` line, with: +# DATABASES = { +# "default": { +# "ENGINE": "sentry.db.postgres", +# "NAME": "postgres", +# "USER": "postgres", +# "PASSWORD": "", +# "HOST": "postgres", +# "PORT": "", +# } +# } + +# Create the replacement text in a temp file +cat >/tmp/sentry_conf_py_db_config <<'EOF' +DATABASES = { + "default": { + "ENGINE": "sentry.db.postgres", + "NAME": "postgres", + "USER": "postgres", + "PASSWORD": "", + "HOST": "postgres", + "PORT": "", + } +} +EOF + +# Replace the block +sed -i '/^DATABASES = {$/,/^}$/{ + /^DATABASES = {$/r /tmp/sentry_conf_py_db_config + d +}' $SENTRY_CONFIG_PY + +# Clean up +rm /tmp/sentry_conf_py_db_config + +source install/migrate-pgbouncer.sh + +# Extract actual content +actual_db_config=$(sed -n '/^DATABASES = {$/,/^}$/p' $SENTRY_CONFIG_PY) + +# Compare +if [ "$actual_db_config" = "$expected_db_config" ]; then + echo "DATABASES section is correct" +else + echo "DATABASES section does not match" + echo "Expected:" + echo "$expected_db_config" + echo "Actual:" + echo "$actual_db_config" + exit 1 +fi + +# Reset the file +rm $SENTRY_CONFIG_PY +cp /tmp/sentry_conf_py $SENTRY_CONFIG_PY + +echo "Test 2 (post 25.9.0 release)" +# Modify the `DATABASES = {` to the next `}` line, with: +# DATABASES = { +# "default": { +# "ENGINE": "sentry.db.postgres", +# "NAME": "postgres", +# "USER": "postgres", +# "PASSWORD": "", +# "HOST": "pgbouncer", +# "PORT": "", +# } +# } + +# Create the replacement text in a temp file +cat >/tmp/sentry_conf_py_db_config <<'EOF' +DATABASES = { + "default": { + "ENGINE": "sentry.db.postgres", + "NAME": "postgres", + "USER": "postgres", + "PASSWORD": "", + "HOST": "pgbouncer", + "PORT": "", + } +} +EOF + +# Replace the block +sed -i '/^DATABASES = {$/,/^}$/{ + /^DATABASES = {$/r /tmp/sentry_conf_py_db_config + d +}' $SENTRY_CONFIG_PY + +# Clean up +rm /tmp/sentry_conf_py_db_config + +source install/migrate-pgbouncer.sh + +# Extract actual content +actual_db_config=$(sed -n '/^DATABASES = {$/,/^}$/p' $SENTRY_CONFIG_PY) + +# Compare +if [ "$actual_db_config" = "$expected_db_config" ]; then + echo "DATABASES section is correct" +else + echo "DATABASES section does not match" + echo "Expected:" + echo "$expected_db_config" + echo "Actual:" + echo "$actual_db_config" + exit 1 +fi + +# Reset the file +rm $SENTRY_CONFIG_PY +cp /tmp/sentry_conf_py $SENTRY_CONFIG_PY + +echo "Test 3 (custom postgres config)" +# Modify the `DATABASES = {` to the next `}` line, with: +# DATABASES = { +# "default": { +# "ENGINE": "sentry.db.postgres", +# "NAME": "postgres", +# "USER": "sentry", +# "PASSWORD": "sentry", +# "HOST": "postgres.internal", +# "PORT": "5432", +# } +# } + +# Create the replacement text in a temp file +cat >/tmp/sentry_conf_py_db_config <<'EOF' +DATABASES = { + "default": { + "ENGINE": "sentry.db.postgres", + "NAME": "postgres", + "USER": "sentry", + "PASSWORD": "sentry", + "HOST": "postgres.internal", + "PORT": "5432", + } +} +EOF + +# Replace the block +sed -i '/^DATABASES = {$/,/^}$/{ + /^DATABASES = {$/r /tmp/sentry_conf_py_db_config + d +}' $SENTRY_CONFIG_PY + +# Clean up +rm /tmp/sentry_conf_py_db_config + +source install/migrate-pgbouncer.sh + +# Extract actual content +actual_db_config=$(sed -n '/^DATABASES = {$/,/^}$/p' $SENTRY_CONFIG_PY) + +# THe file should NOT be modified +if [ "$actual_db_config" = "$expected_db_config" ]; then + echo "DATABASES section SHOULD NOT be modified" + echo "Expected:" + echo "$expected_db_config" + echo "Actual:" + echo "$actual_db_config" + exit 1 +else + echo "DATABASES section is correct" +fi + +# Remove the file +rm $SENTRY_CONFIG_PY /tmp/sentry_conf_py + +report_success diff --git a/install/migrate-pgbouncer.sh b/install/migrate-pgbouncer.sh new file mode 100644 index 00000000000..8c698c72d92 --- /dev/null +++ b/install/migrate-pgbouncer.sh @@ -0,0 +1,82 @@ +echo "${_group}Migrating Postgres config to PGBouncer..." +# If users has this EXACT configuration on their `sentry.conf.py` file: +# ```python +# DATABASES = { +# "default": { +# "ENGINE": "sentry.db.postgres", +# "NAME": "postgres", +# "USER": "postgres", +# "PASSWORD": "", +# "HOST": "postgres", +# "PORT": "", +# } +# } +# ``` +# We need to migrate it to this configuration: +# ```python +# DATABASES = { +# "default": { +# "ENGINE": "sentry.db.postgres", +# "NAME": "postgres", +# "USER": "postgres", +# "PASSWORD": "", +# "HOST": "pgbouncer", +# "PORT": "", +# } +# } +# ``` + +if sed -n '/^DATABASES = {$/,/^}$/p' "$SENTRY_CONFIG_PY" | grep -q '"HOST": "postgres"'; then + apply_config_changes_pgbouncer=0 + if [[ -z "${APPLY_AUTOMATIC_CONFIG_UPDATES:-}" ]]; then + echo + echo "We added PGBouncer to the default Compose stack, and to use that" + echo "you will need to modify your sentry.conf.py file contents." + echo "Do you want us to make this change automatically for you?" + echo + + yn="" + until [ ! -z "$yn" ]; do + read -p "y or n? " yn + case $yn in + y | yes | 1) + export apply_config_changes_pgbouncer=1 + echo + echo -n "Thank you." + ;; + n | no | 0) + export apply_config_changes_pgbouncer=0 + echo + echo -n "Alright, you will need to update your sentry.conf.py file manually before running 'docker compose up' or remove the $(pgbouncer) service if you don't want to use that." + ;; + *) yn="" ;; + esac + done + + echo + echo "To avoid this prompt in the future, use one of these flags:" + echo + echo " --apply-automatic-config-updates" + echo " --no-apply-automatic-config-updates" + echo + echo "or set the APPLY_AUTOMATIC_CONFIG_UPDATES environment variable:" + echo + echo " APPLY_AUTOMATIC_CONFIG_UPDATES=1 to apply automatic updates" + echo " APPLY_AUTOMATIC_CONFIG_UPDATES=0 to not apply automatic updates" + echo + sleep 5 + fi + + if [[ "$APPLY_AUTOMATIC_CONFIG_UPDATES" == 1 || "$apply_config_changes_pgbouncer" == 1 ]]; then + echo "Migrating $SENTRY_CONFIG_PY to use PGBouncer" + sed -i 's/"HOST": "postgres"/"HOST": "pgbouncer"/' "$SENTRY_CONFIG_PY" + echo "Migrated $SENTRY_CONFIG_PY to use PGBouncer" + fi +elif sed -n '/^DATABASES = {$/,/^}$/p' "$SENTRY_CONFIG_PY" | grep -q '"HOST": "pgbouncer"'; then + echo "Found pgbouncer in $SENTRY_CONFIG_PY, I'm assuming you're good! :)" +else + echo "⚠️ You don't have standard configuration for Postgres in $SENTRY_CONFIG_PY, skipping pgbouncer migration. I'm assuming you know what you're doing." + echo " For more information about PGBouncer, refer to https://github.com/getsentry/self-hosted/pull/3884" +fi + +echo "${_endgroup}" diff --git a/install/parse-cli.sh b/install/parse-cli.sh index b342033ca08..aec899c6398 100644 --- a/install/parse-cli.sh +++ b/install/parse-cli.sh @@ -31,6 +31,10 @@ Options: self-hosted instance upstream to Sentry. --container-engine-podman Use podman as the container engine. + --apply-automatic-config-updates + Apply automatic config file updates. + --no-apply-automatic-config-updates + Do not apply automatic config file updates. EOF } @@ -41,6 +45,7 @@ depwarn() { if [ ! -z "${SKIP_USER_PROMPT:-}" ]; then depwarn "SKIP_USER_PROMPT variable" "SKIP_USER_CREATION" SKIP_USER_CREATION="${SKIP_USER_PROMPT}" + APPLY_AUTOMATIC_CONFIG_UPDATES="${SKIP_USER_PROMPT}" fi SKIP_USER_CREATION="${SKIP_USER_CREATION:-}" @@ -49,6 +54,7 @@ SKIP_COMMIT_CHECK="${SKIP_COMMIT_CHECK:-}" REPORT_SELF_HOSTED_ISSUES="${REPORT_SELF_HOSTED_ISSUES:-}" SKIP_SSE42_REQUIREMENTS="${SKIP_SSE42_REQUIREMENTS:-}" CONTAINER_ENGINE_PODMAN="${CONTAINER_ENGINE_PODMAN:-}" +APPLY_AUTOMATIC_CONFIG_UPDATES="${APPLY_AUTOMATIC_CONFIG_UPDATES:-}" while (($#)); do case "$1" in @@ -71,6 +77,8 @@ while (($#)); do --no-report-self-hosted-issues) REPORT_SELF_HOSTED_ISSUES=0 ;; --skip-sse42-requirements) SKIP_SSE42_REQUIREMENTS=1 ;; --container-engine-podman) CONTAINER_ENGINE_PODMAN=1 ;; + --apply-automatic-config-updates) APPLY_AUTOMATIC_CONFIG_UPDATES=1 ;; + --no-apply-automatic-config-updates) APPLY_AUTOMATIC_CONFIG_UPDATES=0 ;; --) ;; *) echo "Unexpected argument: $1. Use --help for usage information." From 440b6585fd888f7e09e051e9c02595ede7d5671c Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 12 Sep 2025 20:24:31 -0400 Subject: [PATCH 262/287] feat(tasks): Remove taskworker option override and add worker healthcheck (#3933) feat(tasks) Remove taskworker option override and add worker healthcheck Along with getsentry/sentry#99374 and this change taskworkers will be enabled by default in self-hosted. I've left the celery workers active to smooth over any tasks that are in-flight during an upgrade. Add a worker healtcheck as we have one now. Refs STREAM-450 --- docker-compose.yml | 4 +++- sentry/sentry.conf.example.py | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6d59d10700b..abd0b98f1a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -721,7 +721,9 @@ services: command: run taskworker-scheduler taskworker: <<: *sentry_defaults - command: run taskworker --concurrency=4 --rpc-host=taskbroker:50051 + command: run taskworker --concurrency=4 --rpc-host=taskbroker:50051 --health-check-file-path=/tmp/health.txt + healthcheck: + <<: *file_healthcheck_defaults vroom: <<: *restart_policy image: "$VROOM_IMAGE" diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 9c58f55752f..7fef845fb5d 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -456,9 +456,3 @@ def get_internal_network(): # } # SENTRY_METRICS_SAMPLE_RATE = 1.0 # Adjust this to your needs, default is 1.0 # SENTRY_METRICS_PREFIX = "sentry." # Adjust this to your needs, default is "sentry." - -######### -# Tasks # -######### -# Disable taskworker and continue using celery. -SENTRY_OPTIONS["taskworker.enabled"] = False From 84f904f7a15698cc5d8f0ce492612cd057f36721 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Sat, 13 Sep 2025 01:48:56 +0100 Subject: [PATCH 263/287] feat: Use S3 node store with seaweedfs (#3498) * feat: Use S3 node store with garage * lol, fix bash * moar bash * lol * hate bash * fix moar bash * Add healthcheck to garage service Co-authored-by: Reinaldy Rafli * revert +x * fix healthcheck, fix config * add env var for garage size * use better compression level * simpler garage config * add migration support * feat: use seaweedfs as nodestore backend (#3842) * feat: seaweedfs as s3 nodestore backend * fix: 'server' was missing for seaweed * feat: remove minimum volume free space * feat: specify hostname on ip * fix: grpc port on seaweed should be `-{service}.port.grpc` instead of `-{service}.grpcPort` * fix: wrong access key & secret key; use localhost for internal comms * fix: create index directory * test: add sentry-seaweedfs volume into expected volumes * debug: aaaaaaaaaaaaaaaaaaaaaaarrrrggggggghhhhhhhhhhhhhhh * test: correct ordering for expected volumes * chore: seaweedfs healthcheck to multiple urls See https://stackoverflow.com/a/14578575/3153224 * chore: add swap for arm64 runners * ci: debug memory issues for arm64 runners * ci: turn off swapfile first Turns out the arm64 runners already have 3GB of swap * feat: nodestore config update behind a prompt/flag * feat: set s3 lifecycle policy * fix: seaweed is a busybox * fix: try xml policy * fix: go back to simplified json * Revert "fix: go back to simplified json" This reverts commit 2f1575dfe33db6f781b09d09b01f5382716b8826. * chore: reword debug lifecycle policy * fix: don't pollute APPLY_AUTOMATIC_CONFIG_UPDATES variable --------- Co-authored-by: Reinaldy Rafli --- _unit-test/create-docker-volumes-test.sh | 1 + docker-compose.yml | 43 +++++++++++ install.sh | 1 + install/bootstrap-s3-nodestore.sh | 91 ++++++++++++++++++++++++ install/create-docker-volumes.sh | 1 + sentry/Dockerfile | 6 +- sentry/sentry.conf.example.py | 25 +++++++ 7 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 install/bootstrap-s3-nodestore.sh diff --git a/_unit-test/create-docker-volumes-test.sh b/_unit-test/create-docker-volumes-test.sh index 2cb9b962a8b..83591d3aa85 100755 --- a/_unit-test/create-docker-volumes-test.sh +++ b/_unit-test/create-docker-volumes-test.sh @@ -14,6 +14,7 @@ sentry-data sentry-kafka sentry-postgres sentry-redis +sentry-seaweedfs sentry-symbolicator" before=$(get_volumes) diff --git a/docker-compose.yml b/docker-compose.yml index abd0b98f1a3..47f0b31844c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,6 +42,8 @@ x-sentry-defaults: &sentry_defaults <<: *depends_on-default smtp: <<: *depends_on-default + seaweedfs: + <<: *depends_on-default snuba-api: <<: *depends_on-default symbolicator: @@ -238,6 +240,45 @@ services: interval: 10s timeout: 10s retries: 30 + seaweedfs: + <<: *restart_policy + image: "chrislusf/seaweedfs:3.96_large_disk" + entrypoint: "weed" + command: >- + server + -filer=true + -filer.port=8888 + -filer.port.grpc=18888 + -filer.defaultReplicaPlacement=000 + -master=true + -master.port=9333 + -master.port.grpc=19333 + -metricsPort=9091 + -s3=true + -s3.port=8333 + -s3.port.grpc=18333 + -volume=true + -volume.dir.idx=/data/idx + -volume.index=leveldbLarge + -volume.max=0 + -volume.preStopSeconds=8 + -volume.readMode=redirect + -volume.port=8080 + -volume.port.grpc=18080 + -ip=127.0.0.1 + -ip.bind=0.0.0.0 + -webdav=false + environment: + AWS_ACCESS_KEY_ID: sentry + AWS_SECRET_ACCESS_KEY: sentry + volumes: + - "sentry-seaweedfs:/data" + healthcheck: + test: ["CMD", "wget", "-q", "-O-", "http://seaweedfs:8080/healthz", "http://seaweedfs:9333/cluster/healthz", "http://seaweedfs:8333/healthz"] + interval: 30s + timeout: 20s + retries: 5 + start_period: 60s snuba-api: <<: *snuba_defaults healthcheck: @@ -801,6 +842,8 @@ volumes: external: true sentry-symbolicator: external: true + sentry-seaweedfs: + external: true # This volume stores JS SDK assets and the data inside this volume should # be cleaned periodically on upgrades. sentry-nginx-www: diff --git a/install.sh b/install.sh index d7f8a036f4e..c6b3a62a178 100755 --- a/install.sh +++ b/install.sh @@ -36,6 +36,7 @@ source install/ensure-relay-credentials.sh source install/generate-secret-key.sh source install/update-docker-images.sh source install/build-docker-images.sh +source install/bootstrap-s3-nodestore.sh source install/bootstrap-snuba.sh source install/upgrade-postgres.sh source install/ensure-correct-permissions-profiles-dir.sh diff --git a/install/bootstrap-s3-nodestore.sh b/install/bootstrap-s3-nodestore.sh new file mode 100644 index 00000000000..bf767a78295 --- /dev/null +++ b/install/bootstrap-s3-nodestore.sh @@ -0,0 +1,91 @@ +echo "${_group}Bootstrapping seaweedfs (node store)..." + +$dc up --wait seaweedfs postgres +$dc exec seaweedfs apk add --no-cache s3cmd +$dc exec seaweedfs mkdir -p /data/idx/ +s3cmd="$dc exec seaweedfs s3cmd" + +bucket_list=$($s3cmd --access_key=sentry --secret_key=sentry --no-ssl --region=us-east-1 --host=localhost:8333 --host-bucket='localhost:8333/%(bucket)' ls) + +if [[ $($bucket_list | tail -1 | awk '{print $3}') != 's3://nodestore' ]]; then + apply_config_changes_nodestore=0 + # Only touch if no existing nodestore config is found + if ! grep -q "SENTRY_NODESTORE" $SENTRY_CONFIG_PY; then + if [[ -z "${APPLY_AUTOMATIC_CONFIG_UPDATES:-}" ]]; then + echo + echo "We want to migrate Nodestore backend from Postgres to S3 which will" + echo "help reducing Postgres storage issues. In order to do that, we need" + echo "to modify your sentry.conf.py file contents." + echo "Do you want us to do it automatically for you?" + echo + + yn="" + until [ ! -z "$yn" ]; do + read -p "y or n? " yn + case $yn in + y | yes | 1) + export apply_config_changes_nodestore=1 + echo + echo -n "Thank you." + ;; + n | no | 0) + export apply_config_changes_nodestore=0 + echo + echo -n "Alright, you will need to update your sentry.conf.py file manually before running 'docker compose up'." + ;; + *) yn="" ;; + esac + done + + echo + echo "To avoid this prompt in the future, use one of these flags:" + echo + echo " --apply-automatic-config-updates" + echo " --no-apply-automatic-config-updates" + echo + echo "or set the APPLY_AUTOMATIC_CONFIG_UPDATES environment variable:" + echo + echo " APPLY_AUTOMATIC_CONFIG_UPDATES=1 to apply automatic updates" + echo " APPLY_AUTOMATIC_CONFIG_UPDATES=0 to not apply automatic updates" + echo + sleep 5 + fi + + if [[ "$APPLY_AUTOMATIC_CONFIG_UPDATES" == 1 || "$apply_config_changes_nodestore" == 1 ]]; then + nodestore_config=$(sed -n '/SENTRY_NODESTORE/,/[}]/{p}' sentry/sentry.conf.example.py) + if [[ $($dc exec postgres psql -qAt -U postgres -c "select exists (select * from nodestore_node limit 1)") = "f" ]]; then + nodestore_config=$(echo -e "$nodestore_config" | sed '$s/\}/ "read_through": True,\n "delete_through": True,\n\}/') + fi + echo "$nodestore_config" >>$SENTRY_CONFIG_PY + fi + fi + + $dc exec seaweedfs mkdir -p /data/idx/ + $s3cmd --access_key=sentry --secret_key=sentry --no-ssl --region=us-east-1 --host=localhost:8333 --host-bucket='localhost:8333/%(bucket)' mb s3://nodestore + + # XXX(aldy505): Should we refactor this? + lifecycle_policy=$( + cat < + + + Sentry-Nodestore-Rule + Enabled + + + $SENTRY_EVENT_RETENTION_DAYS + + + +EOF + ) + $dc exec seaweedfs sh -c "printf '%s' '$lifecycle_policy' > /tmp/nodestore-lifecycle-policy.xml" + $s3cmd --access_key=sentry --secret_key=sentry --no-ssl --region=us-east-1 --host=localhost:8333 --host-bucket='localhost:8333/%(bucket)' setlifecycle /tmp/nodestore-lifecycle-policy.xml s3://nodestore + + echo "Making sure the bucket lifecycle policy is all set up correctly..." + $s3cmd --access_key=sentry --secret_key=sentry --no-ssl --region=us-east-1 --host=localhost:8333 --host-bucket='localhost:8333/%(bucket)' getlifecycle s3://nodestore +else + echo "Node store already exists, skipping..." +fi + +echo "${_endgroup}" diff --git a/install/create-docker-volumes.sh b/install/create-docker-volumes.sh index fdbecc2288b..c0437c5563b 100644 --- a/install/create-docker-volumes.sh +++ b/install/create-docker-volumes.sh @@ -17,5 +17,6 @@ echo "Created $(create_volume sentry-kafka)." echo "Created $(create_volume sentry-postgres)." echo "Created $(create_volume sentry-redis)." echo "Created $(create_volume sentry-symbolicator)." +echo "Created $(create_volume sentry-seaweedfs)." echo "${_endgroup}" diff --git a/sentry/Dockerfile b/sentry/Dockerfile index 557046f143d..40398a773fb 100644 --- a/sentry/Dockerfile +++ b/sentry/Dockerfile @@ -1,13 +1,15 @@ ARG SENTRY_IMAGE FROM ${SENTRY_IMAGE} +RUN pip install https://github.com/stayallive/sentry-nodestore-s3/archive/main.zip + COPY . /usr/src/sentry RUN if [ -s /usr/src/sentry/enhance-image.sh ]; then \ /usr/src/sentry/enhance-image.sh; \ -fi + fi RUN if [ -s /usr/src/sentry/requirements.txt ]; then \ echo "sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://develop.sentry.dev/self-hosted/#enhance-sentry-image"; \ pip install -r /usr/src/sentry/requirements.txt; \ -fi + fi diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 7fef845fb5d..9419e6a74e3 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -95,6 +95,31 @@ def get_internal_network(): # See https://develop.sentry.dev/self-hosted/experimental/errors-only/ SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete" +################ +# Node Storage # +################ + +# Sentry uses an abstraction layer called "node storage" to store raw events. +# Previously, it used PostgreSQL as the backend, but this didn't scale for +# high-throughput environments. Read more about this in the documentation: +# https://develop.sentry.dev/backend/application-domains/nodestore/ +# +# Through this setting, you can use the provided blob storage or +# your own S3-compatible API from your infrastructure. +# Other backend implementations for node storage developed by the community +# are available in public GitHub repositories. + +SENTRY_NODESTORE = "sentry_nodestore_s3.S3PassthroughDjangoNodeStorage" +SENTRY_NODESTORE_OPTIONS = { + "compression": True, + "endpoint_url": "http://seaweedfs:8333/", + "bucket_path": "nodestore", + "bucket_name": "nodestore", + "region_name": "us-east-1", + "aws_access_key_id": "sentry", + "aws_secret_access_key": "sentry", +} + ######### # Redis # ######### From b091611020a826441e536589b7619d8c84910c91 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 15 Sep 2025 13:11:25 +0700 Subject: [PATCH 264/287] docs: provide information for SENTRY_AIR_GAP flag on Django config file (#3935) * docs: provide information for SENTRY_AIR_GAP flag on Django config file * Apply suggestions from code review Co-authored-by: Amin Vakil * Apply suggestions from code review Co-authored-by: Amin Vakil --------- Co-authored-by: Amin Vakil --- sentry/sentry.conf.example.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index 9419e6a74e3..f6ef319f6bc 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -95,6 +95,14 @@ def get_internal_network(): # See https://develop.sentry.dev/self-hosted/experimental/errors-only/ SENTRY_SELF_HOSTED_ERRORS_ONLY = env("COMPOSE_PROFILES") != "feature-complete" +# When running in an air-gapped environment, set this to True to entirely disable +# external network calls and features that require Internet connectivity. +# +# Setting the value to False while running in an air-gapped environment will +# cause some containers to raise exceptions. One known example is fetching +# AI model prices from various public APIs. +SENTRY_AIR_GAP = False + ################ # Node Storage # ################ From 407351fb0706a61d20fcbc5d682260f5bbd13267 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:39:10 +0100 Subject: [PATCH 265/287] build(deps): bump actions/create-github-app-token from 2.1.1 to 2.1.4 (#3936) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.1.1 to 2.1.4. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/a8d616148505b5069dccd32f177bb87d7f39123b...67018539274d69449ef7c02e8e71183d1719ab42) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: 2.1.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbd7dfd659f..d4bacd7aed1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Get auth token id: token - uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 + uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 with: app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} From a222e3f8de7cc5881c78fb8722384280995e29d3 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 16 Sep 2025 22:01:13 +0700 Subject: [PATCH 266/287] fix: able to setup nodestore multiple times (#3940) * fix: able to setup nodestore multiple times * fix(test): chmod issues --- _unit-test/bootstrap-s3-seaweed-test.sh | 16 ++++++++++++++++ install/bootstrap-s3-nodestore.sh | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100755 _unit-test/bootstrap-s3-seaweed-test.sh diff --git a/_unit-test/bootstrap-s3-seaweed-test.sh b/_unit-test/bootstrap-s3-seaweed-test.sh new file mode 100755 index 00000000000..2bd052f04c8 --- /dev/null +++ b/_unit-test/bootstrap-s3-seaweed-test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +source _unit-test/_test_setup.sh +source install/dc-detect-version.sh +source install/create-docker-volumes.sh + +# Set the flag to apply automatic updates +export APPLY_AUTOMATIC_CONFIG_UPDATES=1 + +# Here we're just gonna test to run it multiple times +# Only to make sure it doesn't break +for i in $(seq 1 5); do + source install/bootstrap-s3-nodestore.sh +done + +report_success diff --git a/install/bootstrap-s3-nodestore.sh b/install/bootstrap-s3-nodestore.sh index bf767a78295..a41fab48c54 100644 --- a/install/bootstrap-s3-nodestore.sh +++ b/install/bootstrap-s3-nodestore.sh @@ -7,7 +7,7 @@ s3cmd="$dc exec seaweedfs s3cmd" bucket_list=$($s3cmd --access_key=sentry --secret_key=sentry --no-ssl --region=us-east-1 --host=localhost:8333 --host-bucket='localhost:8333/%(bucket)' ls) -if [[ $($bucket_list | tail -1 | awk '{print $3}') != 's3://nodestore' ]]; then +if [[ $(echo "$bucket_list" | tail -1 | awk '{print $3}') != 's3://nodestore' ]]; then apply_config_changes_nodestore=0 # Only touch if no existing nodestore config is found if ! grep -q "SENTRY_NODESTORE" $SENTRY_CONFIG_PY; then From 867004274e8a08560445ea41d5c400bfb3ff4443 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Tue, 16 Sep 2025 15:15:39 +0000 Subject: [PATCH 267/287] release: 25.9.0 --- .env | 14 +++++++------- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.env b/.env index dfe279267f8..a7991959890 100644 --- a/.env +++ b/.env @@ -9,13 +9,13 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly -SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly -RELAY_IMAGE=ghcr.io/getsentry/relay:nightly -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly -VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly -UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly +SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.9.0 +SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.9.0 +RELAY_IMAGE=ghcr.io/getsentry/relay:25.9.0 +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.9.0 +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.9.0 +VROOM_IMAGE=ghcr.io/getsentry/vroom:25.9.0 +UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:25.9.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a8d4528f7..3e1944842e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 25.9.0 + +### Various fixes & improvements + +- fix: able to setup nodestore multiple times (#3940) by @aldy505 +- build(deps): bump actions/create-github-app-token from 2.1.1 to 2.1.4 (#3936) by @dependabot +- docs: provide information for SENTRY_AIR_GAP flag on Django config file (#3935) by @aldy505 +- feat: Use S3 node store with seaweedfs (#3498) by @BYK +- feat(tasks): Remove taskworker option override and add worker healthcheck (#3933) by @markstory +- feat: install script to migrate sentry.conf.py config to use pgbouncer (#3898) by @aldy505 +- chore(deps): bump clickhouse to 25.3 (#3878) by @aldy505 +- feat: enable `issue-views` flag (#3922) by @aldy505 +- feat: query against `eap` dataset instead of `metrics` dataset for spans (#3923) by @aldy505 +- build(deps): bump actions/setup-python from 5 to 6 (#3927) by @dependabot +- Add restart policy to pgbouncer service (#3925) by @frederikspang +- fix(tests): skip logs event test for errors-only (#3915) by @aldy505 +- Improve nginx depends_on policy (#3914) by @aminvakil +- test: run errors-only integration tests (#3910) by @aldy505 +- feat: enable Logs feature (#3912) by @aldy505 +- fix: ensuring vroom permission should be skipped on errors-only (#3911) by @aldy505 +- chore(deps): bump patches version (#3879) by @aldy505 +- Revert "increase postgres max_connections above 100 connections (#2740)" (#3899) by @aminvakil +- Add pgbouncer (#3884) by @frederikspang +- chore: resolve GHA code scanning alerts (#3889) by @aldy505 +- fix(enhancement): search for permissions on docker container instead of host and combine it in one command for performance enhancement (#3890) by @LvckyAPI +- build(deps): bump actions/create-github-app-token from 2.1.0 to 2.1.1 (#3885) by @dependabot +- build(deps): bump actions/checkout from 4 to 5 (#3883) by @dependabot + ## 25.8.0 ### Various fixes & improvements From ec44e9881304449dfaa3253345f153a7454ebcd2 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 17 Sep 2025 14:42:18 +0000 Subject: [PATCH 268/287] build: Set master version to nightly #skip-changelog --- .env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.env b/.env index a7991959890..dfe279267f8 100644 --- a/.env +++ b/.env @@ -9,13 +9,13 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.9.0 -SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.9.0 -RELAY_IMAGE=ghcr.io/getsentry/relay:25.9.0 -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.9.0 -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.9.0 -VROOM_IMAGE=ghcr.io/getsentry/vroom:25.9.0 -UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:25.9.0 +SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly +SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly +RELAY_IMAGE=ghcr.io/getsentry/relay:nightly +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly +VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly +UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 From 8fc3efac78c401db91989eefa05c6dcdd7785f72 Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Fri, 19 Sep 2025 14:07:51 +0000 Subject: [PATCH 269/287] fix: install behind a proxy (#3944) Updated the s3cmd installation command to include proxy environment variables. --- install/bootstrap-s3-nodestore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/bootstrap-s3-nodestore.sh b/install/bootstrap-s3-nodestore.sh index a41fab48c54..15aec23c71e 100644 --- a/install/bootstrap-s3-nodestore.sh +++ b/install/bootstrap-s3-nodestore.sh @@ -1,7 +1,7 @@ echo "${_group}Bootstrapping seaweedfs (node store)..." $dc up --wait seaweedfs postgres -$dc exec seaweedfs apk add --no-cache s3cmd +$dc exec -e "http_proxy=${http_proxy:-}" -e "https_proxy=${https_proxy:-}" -e "no_proxy=${no_proxy:-}" seaweedfs apk add --no-cache s3cmd $dc exec seaweedfs mkdir -p /data/idx/ s3cmd="$dc exec seaweedfs s3cmd" From b4723a0c6f76564c5151646450d628cd5d585f92 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 19 Sep 2025 10:47:41 -0400 Subject: [PATCH 270/287] chore(tasks): Remove the worker and cron containers (#3946) These are replaced by the `taskworker` and `taskscheduler` containers Refs STREAM-433 --- docker-compose.yml | 14 -------------- sentry/sentry.conf.example.py | 19 ------------------- 2 files changed, 33 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 47f0b31844c..319e4be5bf8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -512,20 +512,6 @@ services: - "-c" # Courtesy of https://unix.stackexchange.com/a/234089/108960 - 'exec 3<>/dev/tcp/127.0.0.1/9000 && echo -e "GET /_health/ HTTP/1.1\r\nhost: 127.0.0.1\r\n\r\n" >&3 && grep ok -s -m 1 <&3' - cron: - <<: *sentry_defaults - command: run cron - worker: - <<: *sentry_defaults - command: run worker - healthcheck: - <<: *healthcheck_defaults - test: - - CMD - - sentry - - exec - - -c - - 'from sentry.celery import app; import os; dest="celery@{}".format(os.environ["HOSTNAME"]); print(app.control.ping(destination=[dest], timeout=5)[0][dest]["ok"])' events-consumer: <<: *sentry_defaults command: run consumer ingest-events --consumer-group ingest-consumer --healthcheck-file-path /tmp/health.txt diff --git a/sentry/sentry.conf.example.py b/sentry/sentry.conf.example.py index f6ef319f6bc..38147f7bb02 100644 --- a/sentry/sentry.conf.example.py +++ b/sentry/sentry.conf.example.py @@ -141,25 +141,6 @@ def get_internal_network(): } } -######### -# Queue # -######### - -# See https://develop.sentry.dev/services/queue/ for more -# information on configuring your queue broker and workers. Sentry relies -# on a Python framework called Celery to manage queues. - -rabbitmq_host = None -if rabbitmq_host: - BROKER_URL = "amqp://{username}:{password}@{host}/{vhost}".format( - username="guest", password="guest", host=rabbitmq_host, vhost="/" - ) -else: - BROKER_URL = "redis://:{password}@{host}:{port}/{db}".format( - **SENTRY_OPTIONS["redis.clusters"]["default"]["hosts"][0] - ) - - ######### # Cache # ######### From 18b7a4d1cccbdbd6b7d278761383160e305e0765 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Tue, 23 Sep 2025 17:31:30 +0330 Subject: [PATCH 271/287] Respect uppercase proxy variables (#3949) * Respect uppercase proxy variables * Put http_proxy first --- cron/Dockerfile | 2 ++ docker-compose.yml | 2 +- install/bootstrap-s3-nodestore.sh | 2 +- install/dc-detect-version.sh | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cron/Dockerfile b/cron/Dockerfile index c14304eb89a..26c46a6f68f 100644 --- a/cron/Dockerfile +++ b/cron/Dockerfile @@ -1,6 +1,8 @@ ARG BASE_IMAGE FROM ${BASE_IMAGE} USER 0 +RUN if [ -n "${HTTP_PROXY}" ]; then echo "Acquire::http::proxy \"${HTTP_PROXY}\";" >> /etc/apt/apt.conf; fi +RUN if [ -n "${HTTPS_PROXY}" ]; then echo "Acquire::https::proxy \"${HTTPS_PROXY}\";" >> /etc/apt/apt.conf; fi RUN if [ -n "${http_proxy}" ]; then echo "Acquire::http::proxy \"${http_proxy}\";" >> /etc/apt/apt.conf; fi RUN if [ -n "${https_proxy}" ]; then echo "Acquire::https::proxy \"${https_proxy}\";" >> /etc/apt/apt.conf; fi RUN apt-get update && apt-get install -y --no-install-recommends cron && \ diff --git a/docker-compose.yml b/docker-compose.yml index 319e4be5bf8..2c3cd9264e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -235,7 +235,7 @@ services: # Manually override any http_proxy envvar that might be set, because # this wget does not support no_proxy. See: # https://github.com/getsentry/self-hosted/issues/1537 - "http_proxy='' wget -nv -t1 --spider 'http://localhost:8123/' || exit 1", + "HTTP_PROXY='' http_proxy='' wget -nv -t1 --spider 'http://localhost:8123/' || exit 1", ] interval: 10s timeout: 10s diff --git a/install/bootstrap-s3-nodestore.sh b/install/bootstrap-s3-nodestore.sh index 15aec23c71e..fb88c2e4786 100644 --- a/install/bootstrap-s3-nodestore.sh +++ b/install/bootstrap-s3-nodestore.sh @@ -1,7 +1,7 @@ echo "${_group}Bootstrapping seaweedfs (node store)..." $dc up --wait seaweedfs postgres -$dc exec -e "http_proxy=${http_proxy:-}" -e "https_proxy=${https_proxy:-}" -e "no_proxy=${no_proxy:-}" seaweedfs apk add --no-cache s3cmd +$dc exec -e "HTTP_PROXY=${HTTP_PROXY:-}" -e "HTTPS_PROXY=${HTTPS_PROXY:-}" -e "NO_PROXY=${NO_PROXY:-}" -e "http_proxy=${http_proxy:-}" -e "https_proxy=${https_proxy:-}" -e "no_proxy=${no_proxy:-}" seaweedfs apk add --no-cache s3cmd $dc exec seaweedfs mkdir -p /data/idx/ s3cmd="$dc exec seaweedfs s3cmd" diff --git a/install/dc-detect-version.sh b/install/dc-detect-version.sh index 6f9d4230d77..f7a4cbdda97 100644 --- a/install/dc-detect-version.sh +++ b/install/dc-detect-version.sh @@ -47,9 +47,9 @@ else dc="$dc_base $NO_ANSI" fi -proxy_args="--build-arg http_proxy=${http_proxy:-} --build-arg https_proxy=${https_proxy:-} --build-arg no_proxy=${no_proxy:-}" +proxy_args="--build-arg HTTP_PROXY=${HTTP_PROXY:-} --build-arg HTTPS_PROXY=${HTTPS_PROXY:-} --build-arg NO_PROXY=${NO_PROXY:-} --build-arg http_proxy=${http_proxy:-} --build-arg https_proxy=${https_proxy:-} --build-arg no_proxy=${no_proxy:-}" if [[ "$CONTAINER_ENGINE" == "podman" ]]; then - proxy_args_dc="--podman-build-args http_proxy=${http_proxy:-},https_proxy=${https_proxy:-},no_proxy=${no_proxy:-}" + proxy_args_dc="--podman-build-args HTTP_PROXY=${HTTP_PROXY:-},HTTPS_PROXY=${HTTPS_PROXY:-},NO_PROXY=${NO_PROXY:-},http_proxy=${http_proxy:-},https_proxy=${https_proxy:-},no_proxy=${no_proxy:-}" # Disable pod creation as these are one-off commands and creating a pod # prints its pod id to stdout which is messing with the output that we # rely on various places such as configuration generation From 0ffcf0e1269e0995949cc04e3154d02430f3b6ff Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 24 Sep 2025 19:44:49 -0400 Subject: [PATCH 272/287] chore(tasks) Remove reference to celery (#3962) --- install/_lib.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/_lib.sh b/install/_lib.sh index 3beb18e38db..29097049576 100644 --- a/install/_lib.sh +++ b/install/_lib.sh @@ -53,7 +53,7 @@ export SENTRY_CONFIG_PY=sentry/sentry.conf.py export SENTRY_CONFIG_YML=sentry/config.yml # Increase the default 10 second SIGTERM timeout -# to ensure celery queues are properly drained +# to ensure task queues are properly drained # between upgrades as task signatures may change across # versions export STOP_TIMEOUT=60 # seconds From 37918fb464176249c1f77440f767aa2fc9371df0 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Fri, 26 Sep 2025 17:25:35 +0330 Subject: [PATCH 273/287] Fix swap allocation in integration test (#3972) * Check free space prior to offing swap * Get hint about packages disk usage * Comment self-hosted installation to speed up * Revert "Comment self-hosted installation to speed up" This reverts commit 257e57f9d067ec14dc1b04df9d53a91514aa96d1. * Comment self-hosted installation * Revert "Comment self-hosted installation" This reverts commit f22beff1e57bc305548c1bfafbb73e4f049ccf5b. * Revert "Get hint about packages disk usage" This reverts commit f4fd43c31db469d5a89b2615440d1577dfd0a5eb. * Remove jdk packages and their depended ons * Remove man-db * Remove microsoft packages * Remove php packages * Cleanup runner image prior to everything * Remove all *jre* packages * Remove jdk packages more aggressively * Remove haskell directory * Remove df -h commands --- action.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/action.yaml b/action.yaml index 563e06d40d6..a41f453336a 100644 --- a/action.yaml +++ b/action.yaml @@ -42,6 +42,12 @@ runs: echo "COMPOSE_PROFILES=$COMPOSE_PROFILES" >> ${{ github.action_path }}/.env fi + - name: Cleanup runner image + shell: bash + run: | + ### Inspired by https://github.com/endersonmenezes/free-disk-space ### + sudo rm -rf /usr/local/.ghcup + - name: Setup dev environment shell: bash run: | From 2d11de51e57be9232c4e3c5e5943a9551d99d25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klemens=20B=C3=B6swirth?= <23529132+kodebach@users.noreply.github.com> Date: Fri, 26 Sep 2025 16:13:59 +0200 Subject: [PATCH 274/287] fix: logic error in s3 install script (#3965) --- install/bootstrap-s3-nodestore.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/bootstrap-s3-nodestore.sh b/install/bootstrap-s3-nodestore.sh index fb88c2e4786..c2cf91669f5 100644 --- a/install/bootstrap-s3-nodestore.sh +++ b/install/bootstrap-s3-nodestore.sh @@ -53,7 +53,7 @@ if [[ $(echo "$bucket_list" | tail -1 | awk '{print $3}') != 's3://nodestore' ]] if [[ "$APPLY_AUTOMATIC_CONFIG_UPDATES" == 1 || "$apply_config_changes_nodestore" == 1 ]]; then nodestore_config=$(sed -n '/SENTRY_NODESTORE/,/[}]/{p}' sentry/sentry.conf.example.py) - if [[ $($dc exec postgres psql -qAt -U postgres -c "select exists (select * from nodestore_node limit 1)") = "f" ]]; then + if [[ $($dc exec postgres psql -qAt -U postgres -c "select exists (select * from nodestore_node limit 1)") = "t" ]]; then nodestore_config=$(echo -e "$nodestore_config" | sed '$s/\}/ "read_through": True,\n "delete_through": True,\n\}/') fi echo "$nodestore_config" >>$SENTRY_CONFIG_PY From 1640300332428e0d7d8afe74b75333e37886e346 Mon Sep 17 00:00:00 2001 From: Javan Eskander Date: Sat, 27 Sep 2025 00:16:45 +1000 Subject: [PATCH 275/287] fix: Unset the proxy when performing the seaweedfs health check (#3959) --- docker-compose.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 2c3cd9264e3..4c72da11d52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -274,7 +274,13 @@ services: volumes: - "sentry-seaweedfs:/data" healthcheck: - test: ["CMD", "wget", "-q", "-O-", "http://seaweedfs:8080/healthz", "http://seaweedfs:9333/cluster/healthz", "http://seaweedfs:8333/healthz"] + test: [ + "CMD-SHELL", + # Manually override any http_proxy envvar that might be set, because + # this wget does not support no_proxy. See: + # https://github.com/getsentry/self-hosted/issues/1537 + "http_proxy='' wget -q -O- http://seaweedfs:8080/healthz http://seaweedfs:9333/cluster/healthz http://seaweedfs:8333/healthz || exit 1", + ] interval: 30s timeout: 20s retries: 5 From 663b86c108e4bb690987361c65e7e182bee45ce6 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Fri, 26 Sep 2025 18:15:09 +0330 Subject: [PATCH 276/287] ref: Remove proxy_next_upstream directives (#3973) --- nginx.conf | 2 -- 1 file changed, 2 deletions(-) diff --git a/nginx.conf b/nginx.conf index 94d7964d58d..95e96226c1c 100644 --- a/nginx.conf +++ b/nginx.conf @@ -41,8 +41,6 @@ http { proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; - proxy_next_upstream error timeout invalid_header http_502 http_503 non_idempotent; - proxy_next_upstream_tries 2; # Docker default address pools # https://github.com/moby/libnetwork/blob/3797618f9a38372e8107d8c06f6ae199e1133ae8/ipamutils/utils.go#L10-L22 From 79b46fb495e2ce24a902e6bbc1eb14187162613b Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Fri, 26 Sep 2025 23:16:52 +0700 Subject: [PATCH 277/287] fix(actions): include arch and compose_profiles information on cache keys (#3974) Closes #3964 --------- Co-authored-by: Burak Yigit Kaya --- action.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/action.yaml b/action.yaml index a41f453336a..6b254f2d771 100644 --- a/action.yaml +++ b/action.yaml @@ -80,14 +80,15 @@ runs: echo "SENTRY_MIGRATIONS_MD5=$SENTRY_MIGRATIONS_MD5" >> $GITHUB_OUTPUT SNUBA_MIGRATIONS_MD5=$(docker run --rm --entrypoint bash $SNUBA_IMAGE -c '{ ls -Rv1rpq snuba/snuba_migrations/**/*.py; grep -Poz "(?s)(?<=class Topic\\(Enum\\):\\n).+?(?=\\n\\n\\n)" snuba/utils/streams/topics.py; }' | md5sum | cut -d ' ' -f 1) echo "SNUBA_MIGRATIONS_MD5=$SNUBA_MIGRATIONS_MD5" >> $GITHUB_OUTPUT + echo "ARCH=$(uname -m)" >> $GITHUB_OUTPUT - name: Restore Sentry Volume Cache id: restore_cache_sentry uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-sentry-v2-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} + key: db-volumes-sentry-v3-${{ steps.cache_key.outputs.ARCH }}-${{ inputs.compose_profiles }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-sentry-v1- + key: db-volumes-sentry-v3-${{ steps.cache_key.outputs.ARCH }}-${{ inputs.compose_profiles }} volumes: | sentry-postgres @@ -95,9 +96,9 @@ runs: id: restore_cache_snuba uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-snuba-v2-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + key: db-volumes-snuba-v3-${{ steps.cache_key.outputs.ARCH }}-${{ inputs.compose_profiles }}-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-snuba-v1- + key: db-volumes-snuba-v3-${{ steps.cache_key.outputs.ARCH }}-${{ inputs.compose_profiles }} volumes: | sentry-clickhouse @@ -105,11 +106,10 @@ runs: id: restore_cache_kafka uses: BYK/docker-volume-cache-action/restore@be89365902126f508dcae387a32ec3712df6b1cd with: - key: db-volumes-kafka-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} + key: db-volumes-kafka-v2-${{ steps.cache_key.outputs.ARCH }}-${{ inputs.compose_profiles }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} restore-keys: | - db-volumes-kafka-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }}-${{ steps.cache_key.outputs.SNUBA_MIGRATIONS_MD5 }} - db-volumes-kafka-v1-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }}- - db-volumes-kafka-v1- + db-volumes-kafka-v2-${{ steps.cache_key.outputs.ARCH }}-${{ inputs.compose_profiles }}-${{ steps.cache_key.outputs.SENTRY_MIGRATIONS_MD5 }} + db-volumes-kafka-v2-${{ steps.cache_key.outputs.ARCH }}-${{ inputs.compose_profiles }} volumes: | sentry-kafka From 143f58579f29d2ceb8969299b9a67aec53390549 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 1 Oct 2025 16:14:32 +0700 Subject: [PATCH 278/287] ref: use dedicated `healthcheck` command for symbolicator & remove cron for `symbolicator-cleanup` (#3979) --- docker-compose.yml | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4c72da11d52..321072cf035 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -479,6 +479,7 @@ services: symbolicator: <<: *restart_policy image: "$SYMBOLICATOR_IMAGE" + command: run -c /etc/symbolicator/config.yml volumes: - "sentry-symbolicator:/data" - type: bind @@ -487,23 +488,17 @@ services: target: /etc/symbolicator healthcheck: <<: *healthcheck_defaults - test: - - "CMD" - - "/bin/bash" - - "-c" - # Courtesy of https://unix.stackexchange.com/a/234089/108960 - - 'exec 3<>/dev/tcp/127.0.0.1/3021 && echo -e "GET /healthcheck HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n" >&3 && grep OK -s -m 1 <&3' - command: run -c /etc/symbolicator/config.yml + test: ["CMD", "/bin/symbolicator", "healthcheck", "-c", "/etc/symbolicator/config.yml"] symbolicator-cleanup: - <<: [*restart_policy, *pull_policy] - image: symbolicator-cleanup-self-hosted-local - build: - context: ./cron - args: - BASE_IMAGE: "$SYMBOLICATOR_IMAGE" - command: '"55 23 * * * gosu symbolicator symbolicator cleanup"' + <<: *restart_policy + image: "$SYMBOLICATOR_IMAGE" + command: "cleanup -c /etc/symbolicator/config.yml --repeat 1h" volumes: - "sentry-symbolicator:/data" + - type: bind + read_only: true + source: ./symbolicator + target: /etc/symbolicator web: <<: *sentry_defaults ulimits: From 5cf07c888887516918f5c17810e8f7bae5ac7a4a Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 1 Oct 2025 16:54:00 +0700 Subject: [PATCH 279/287] ref: add `continue-on-error` for codecov action on self-hosted integration tests (#3978) --- action.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/action.yaml b/action.yaml index 6b254f2d771..7f7a98f84fc 100644 --- a/action.yaml +++ b/action.yaml @@ -186,8 +186,9 @@ runs: pytest -x --cov --junitxml=junit.xml _integration-test/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 if: inputs.CODECOV_TOKEN + continue-on-error: true + uses: codecov/codecov-action@v5 with: directory: ${{ github.action_path }} token: ${{ inputs.CODECOV_TOKEN }} @@ -195,6 +196,7 @@ runs: - name: Upload test results to Codecov if: inputs.CODECOV_TOKEN && !cancelled() + continue-on-error: true uses: codecov/test-results-action@v1 with: directory: ${{ github.action_path }} From 0ed356938badadaff3b1a08b423ae2876639d1d6 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Wed, 8 Oct 2025 00:50:07 +0330 Subject: [PATCH 280/287] Bump redis 6.2.20-alpine (#3988) https://redis.io/blog/security-advisory-cve-2025-49844/ --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 321072cf035..fdb4f8c7914 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,7 +117,7 @@ services: test: echo stats | nc 127.0.0.1 11211 redis: <<: *restart_policy - image: "redis:6.2.19-alpine" + image: "redis:6.2.20-alpine" healthcheck: <<: *healthcheck_defaults test: redis-cli ping | grep PONG From b77c5a809722bf41bdcdbdef8dfdbc759d6cc90b Mon Sep 17 00:00:00 2001 From: Joris Bayer Date: Thu, 9 Oct 2025 13:43:40 +0200 Subject: [PATCH 281/287] chore(spans): Remove old snuba-spans consumer (#3989) --- docker-compose.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fdb4f8c7914..64781e6675d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -450,13 +450,6 @@ services: <<: *file_healthcheck_defaults profiles: - feature-complete - snuba-spans-consumer: - <<: *snuba_defaults - command: rust-consumer --storage spans --consumer-group snuba-spans-consumers --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --health-check-file /tmp/health.txt - healthcheck: - <<: *file_healthcheck_defaults - profiles: - - feature-complete snuba-eap-items-consumer: <<: *snuba_defaults command: rust-consumer --storage eap_items --consumer-group eap_items_group --auto-offset-reset=latest --max-batch-time-ms 1000 --no-strict-offset-reset --use-rust-processor --health-check-file /tmp/health.txt From e1f003313e829fb662a8f8845da90275f297f1c5 Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Fri, 10 Oct 2025 05:44:41 +0330 Subject: [PATCH 282/287] Remove symbolicator external volume (#3992) * Remove symbolicator volume creation command * Remove symbolicator volume * Create sentry-symbolicator on docker compose up * Pass volume name to remove_command * Remove sentry-symbolicator from unit test --- _unit-test/create-docker-volumes-test.sh | 3 +-- docker-compose.yml | 5 +++-- install/create-docker-volumes.sh | 1 - install/turn-things-off.sh | 7 +++++++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/_unit-test/create-docker-volumes-test.sh b/_unit-test/create-docker-volumes-test.sh index 83591d3aa85..5de88e420f7 100755 --- a/_unit-test/create-docker-volumes-test.sh +++ b/_unit-test/create-docker-volumes-test.sh @@ -14,8 +14,7 @@ sentry-data sentry-kafka sentry-postgres sentry-redis -sentry-seaweedfs -sentry-symbolicator" +sentry-seaweedfs" before=$(get_volumes) diff --git a/docker-compose.yml b/docker-compose.yml index 64781e6675d..f53c3c3a8ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -820,10 +820,11 @@ volumes: external: true sentry-clickhouse: external: true - sentry-symbolicator: - external: true sentry-seaweedfs: external: true + # The volume stores cached version of debug symbols, source maps etc. Upon + # removal symbolicator will re-download them. + sentry-symbolicator: # This volume stores JS SDK assets and the data inside this volume should # be cleaned periodically on upgrades. sentry-nginx-www: diff --git a/install/create-docker-volumes.sh b/install/create-docker-volumes.sh index c0437c5563b..9241caa0e1b 100644 --- a/install/create-docker-volumes.sh +++ b/install/create-docker-volumes.sh @@ -16,7 +16,6 @@ echo "Created $(create_volume sentry-data)." echo "Created $(create_volume sentry-kafka)." echo "Created $(create_volume sentry-postgres)." echo "Created $(create_volume sentry-redis)." -echo "Created $(create_volume sentry-symbolicator)." echo "Created $(create_volume sentry-seaweedfs)." echo "${_endgroup}" diff --git a/install/turn-things-off.sh b/install/turn-things-off.sh index 1c70ce392c8..576220665ca 100644 --- a/install/turn-things-off.sh +++ b/install/turn-things-off.sh @@ -17,4 +17,11 @@ else fi fi +remove_volume() { + remove_command="$CONTAINER_ENGINE volume remove -f" + $remove_command $1 +} + +echo "Removed $(remove_volume sentry-symbolicator)." + echo "${_endgroup}" From da1f546bfb37c5f3962f956908a10192170199fa Mon Sep 17 00:00:00 2001 From: Amin Vakil Date: Sat, 11 Oct 2025 03:37:40 +0330 Subject: [PATCH 283/287] Remove symbolicator volume once (#3994) * Remove sentry-symbolicator only if it exists * Remove unnecessary volume remove force flag --- install/turn-things-off.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/install/turn-things-off.sh b/install/turn-things-off.sh index 576220665ca..fc0ddf38d48 100644 --- a/install/turn-things-off.sh +++ b/install/turn-things-off.sh @@ -17,11 +17,16 @@ else fi fi +exists_volume() { + $CONTAINER_ENGINE volume inspect $1 >&/dev/null +} remove_volume() { - remove_command="$CONTAINER_ENGINE volume remove -f" + remove_command="$CONTAINER_ENGINE volume remove" $remove_command $1 } -echo "Removed $(remove_volume sentry-symbolicator)." +if exists_volume sentry-symbolicator; then + echo "Removed $(remove_volume sentry-symbolicator)." +fi echo "${_endgroup}" From c55b93bfa12ee9c551ad141a15ee15a9e3c0c2d2 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 15 Oct 2025 05:01:10 +0700 Subject: [PATCH 284/287] fix: missing `-dir` flag for seaweedfs (#3991) --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index f53c3c3a8ee..e7400ae61a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -246,6 +246,7 @@ services: entrypoint: "weed" command: >- server + -dir=/data -filer=true -filer.port=8888 -filer.port.grpc=18888 From 60da8f6eb00f41024f3b13356034023777f8c982 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Wed, 15 Oct 2025 20:31:03 +0700 Subject: [PATCH 285/287] fix: geoip standalone script should check on CONTAINER_ENGINE variable first (#3982) --- install/geoip.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/install/geoip.sh b/install/geoip.sh index 0d1b2efc0aa..9cea3082409 100644 --- a/install/geoip.sh +++ b/install/geoip.sh @@ -1,5 +1,18 @@ echo "${_group}Setting up GeoIP integration ..." +# If `$CONTAINER_ENGINE` is not set, we assume that we are running this script independently +# to update the geoip database as written on the documentation. +# Therefore we need to `source _detect-container-engine.sh` to detect the container engine. +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) +if [[ -z "$CONTAINER_ENGINE" ]]; then + if [[ -f "$script_dir/_detect-container-engine.sh" ]]; then + source $script_dir/_detect-container-engine.sh + else + echo "Error: Cannot find _detect-container-engine.sh. Defaulting to docker." + export CONTAINER_ENGINE="docker" + fi +fi + install_geoip() { local mmdb=geoip/GeoLite2-City.mmdb local conf=geoip/GeoIP.conf From 77a33346db67a07bf5de1ee8eabed23e8622976b Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 15 Oct 2025 18:07:56 +0000 Subject: [PATCH 286/287] release: 25.10.0 --- .env | 14 +++++++------- CHANGELOG.md | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.env b/.env index dfe279267f8..5d2bae9abc6 100644 --- a/.env +++ b/.env @@ -9,13 +9,13 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly -SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly -RELAY_IMAGE=ghcr.io/getsentry/relay:nightly -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly -VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly -UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly +SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.10.0 +SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.10.0 +RELAY_IMAGE=ghcr.io/getsentry/relay:25.10.0 +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.10.0 +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.10.0 +VROOM_IMAGE=ghcr.io/getsentry/vroom:25.10.0 +UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:25.10.0 HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1944842e2..811ebf38e5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 25.10.0 + +### Various fixes & improvements + +- fix: geoip standalone script should check on CONTAINER_ENGINE variable first (#3982) by @aldy505 +- fix: missing `-dir` flag for seaweedfs (#3991) by @aldy505 +- Remove symbolicator volume once (#3994) by @aminvakil +- Remove symbolicator external volume (#3992) by @aminvakil +- chore(spans): Remove old snuba-spans consumer (#3989) by @jjbayer +- Bump redis 6.2.20-alpine (#3988) by @aminvakil +- ref: add `continue-on-error` for codecov action on self-hosted integration tests (#3978) by @aldy505 +- ref: use dedicated `healthcheck` command for symbolicator & remove cron for `symbolicator-cleanup` (#3979) by @aldy505 +- fix(actions): include arch and compose_profiles information on cache keys (#3974) by @aldy505 +- ref: Remove proxy_next_upstream directives (#3973) by @aminvakil +- fix: Unset the proxy when performing the seaweedfs health check (#3959) by @SteppingHat +- fix: logic error in s3 install script (#3965) by @kodebach +- Fix swap allocation in integration test (#3972) by @aminvakil +- chore(tasks) Remove reference to celery (#3962) by @markstory +- Respect uppercase proxy variables (#3949) by @aminvakil +- chore(tasks): Remove the worker and cron containers (#3946) by @markstory +- fix: install behind a proxy (#3944) by @moroine + ## 25.9.0 ### Various fixes & improvements From 6ae40d837a78c2968a28acf952b1beb9911237f8 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 15 Oct 2025 19:18:46 +0000 Subject: [PATCH 287/287] build: Set master version to nightly #skip-changelog --- .env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 5d2bae9abc6..dfe279267f8 100644 --- a/.env +++ b/.env @@ -9,13 +9,13 @@ SENTRY_EVENT_RETENTION_DAYS=90 SENTRY_BIND=9000 # Set SENTRY_MAIL_HOST to a valid FQDN (host/domain name) to be able to send emails! # SENTRY_MAIL_HOST=example.com -SENTRY_IMAGE=ghcr.io/getsentry/sentry:25.10.0 -SNUBA_IMAGE=ghcr.io/getsentry/snuba:25.10.0 -RELAY_IMAGE=ghcr.io/getsentry/relay:25.10.0 -SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:25.10.0 -TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:25.10.0 -VROOM_IMAGE=ghcr.io/getsentry/vroom:25.10.0 -UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:25.10.0 +SENTRY_IMAGE=ghcr.io/getsentry/sentry:nightly +SNUBA_IMAGE=ghcr.io/getsentry/snuba:nightly +RELAY_IMAGE=ghcr.io/getsentry/relay:nightly +SYMBOLICATOR_IMAGE=ghcr.io/getsentry/symbolicator:nightly +TASKBROKER_IMAGE=ghcr.io/getsentry/taskbroker:nightly +VROOM_IMAGE=ghcr.io/getsentry/vroom:nightly +UPTIME_CHECKER_IMAGE=ghcr.io/getsentry/uptime-checker:nightly HEALTHCHECK_INTERVAL=30s HEALTHCHECK_TIMEOUT=1m30s HEALTHCHECK_RETRIES=10