Привет, Хабр!
Меня зовут Антон Пацев, я DevOps-инженер мобильного приложения «Магнит акции и скидки». В этой статье поговорим о Sentry — инструменте для сбора exception, который помогает разработчикам быстро обнаруживать и устранять проблемы, сокращая время выхода новых релизов и повышая удовлетворенность пользователей.

Sentry обладает следующими преимуществами:

  • Мониторинг exception на мобильных устройствах и в браузере.

  • Открытый исходный код: Разработчики могут бесплатно использовать, модифицировать и расширять Sentry, благодаря активному сообществу.

  • Широкая поддержка языков и фреймворков: Поддерживает множество языков и фреймворков, делая его универсальным инструментом.

  • Детальная информация об exception: Предоставляет подробную информацию для быстрой идентификации и исправления проблем.

  • Аналитика и отчеты: Позволяет анализировать статистику exception и создавать детальные отчеты.

  • Самостоятельное развертывание: Возможность развертывания на собственных серверах для контроля над данными.

  • Обширная документация и поддержка сообщества: Помогает быстро начать работу и решать проблемы.

В этой статье рассмотрим минимальную установку Sentry в Kubernetes c clickhouse-operator и kafka-operator, так как установка Sentry в Kubernetes имеет много подводных камней. Используется Clickhouse operator, так как Sentry не работает с более новыми версиями Clickhouse, которые предлагает Яндекс облако.

Для разворачивания Sentry используем Яндекс Облако, Managed Kubernetes, Managed PostgreSQL, Managed Redis, Managed S3.

Настройка HTTPS выходит за рамки данной статьи.

Регистрируем домен на reg.ru. Исправляем домен apatsev.org.ru на ваш домен везде в коде. Версии приложений меняем осторожно, иначе могут быть баги, например https://github.com/ClickHouse/ClickHouse/issues/53749.

В конфигурационных файлах terraform заполняем folder_id, network_id, subnet_id.

Создаем Kubernetes с помощью модуля https://github.com/terraform-yacloud-modules/terraform-yandex-kubernetes

Hidden text
module "iam_accounts" {
  source = "git::https://github.com/terraform-yacloud-modules/terraform-yandex-iam.git//modules/iam-account?ref=v1.0.0"

  name = "iam-sentry-kubernetes"
  folder_roles = [
    "container-registry.images.puller",
    "k8s.clusters.agent",
    "k8s.tunnelClusters.agent",
    "load-balancer.admin",
    "logging.writer",
    "vpc.privateAdmin",
    "vpc.publicAdmin",
    "vpc.user",
  ]
  folder_id = "xxxx"

}

module "kube" {
  source = "git::https://github.com/terraform-yacloud-modules/terraform-yandex-kubernetes.git?ref=v1.1.0"

  folder_id  = "xxxx"
  network_id = "xxxx"

  name = "k8s-sentry"

  service_account_id      = module.iam_accounts.id
  node_service_account_id = module.iam_accounts.id

  master_locations = [
    {
      zone      = "ru-central1-a"
      subnet_id = "xxxx"
    }
  ]

  node_groups = {
    "auto-scale" = {
      nat    = true
      cores  = 6
      memory = 12
      auto_scale = {
        min     = 4
        max     = 6
        initial = 4
      }
    }
  }

  depends_on = [module.iam_accounts]

}

module "address" {
  source = "git::https://github.com/terraform-yacloud-modules/terraform-yandex-address.git?ref=v1.0.0"

  ip_address_name = "sentry-pip"
  folder_id       = "xxxx"
  zone            = "ru-central1-a"
}

module "dns-zone" {
  source = "git::https://github.com/terraform-yacloud-modules/terraform-yandex-dns.git//modules/zone?ref=v1.0.0"

  folder_id = "xxxx"
  name      = "apatsev-org-ru-zone"

  zone             = "apatsev.org.ru." # Точка в конце обязательна
  is_public        = true
  private_networks = ["xxxx"] # network_id
}

module "dns-recordset" {
  source = "git::https://github.com/terraform-yacloud-modules/terraform-yandex-dns.git//modules/recordset?ref=v1.0.0"

  folder_id = "xxxx"
  zone_id   = module.dns-zone.id
  name      = "sentry.apatsev.org.ru." # Точка в конце обязательна
  type      = "A"
  ttl       = 200
  data = [
    module.address.external_ipv4_address
  ]
}

provider "helm" {
  kubernetes {
    host                   = module.kube.external_v4_endpoint
    cluster_ca_certificate = module.kube.cluster_ca_certificate
    exec {
      api_version = "client.authentication.k8s.io/v1beta1"
      args        = ["k8s", "create-token"]
      command     = "yc"
    }
  }
}

resource "helm_release" "ingress_nginx" {
  name             = "ingress-nginx"
  repository       = "https://kubernetes.github.io/ingress-nginx"
  chart            = "ingress-nginx"
  version          = "4.10.1"
  namespace        = "ingress-nginx"
  create_namespace = true
  depends_on       = [module.kube]
  set {
    name  = "controller.service.loadBalancerIP"
    value = module.address.external_ipv4_address
  }

}

Создаем PostgreSQL с помощью модуля https://github.com/terraform-yc-modules/terraform-yc-postgresql

Hidden text
module "db" {
  source = "git::https://github.com/terraform-yc-modules/terraform-yc-postgresql.git"

  folder_id  = "xxxx"
  network_id = "xxxx"
  name       = "sentry-postgresql"

  hosts_definition = [
    {
      zone             = "ru-central1-a"
      assign_public_ip = true
      subnet_id        = "xxxx"
    }
  ]

  postgresql_config = {
    max_connections = 395
  }

  databases = [
    {
      name       = "sentry"
      owner      = "sentry"
      lc_collate = "ru_RU.UTF-8"
      lc_type    = "ru_RU.UTF-8"
      extensions = ["citext"]
    }
  ]

  owners = [
    {
      name     = "sentry"
      password = "sentry-postgresql-password"
    }
  ]
}

output "fqdn_database" {
  value     = "c-${module.db.cluster_id}.rw.mdb.yandexcloud.net"
  sensitive = false
}

output "owners_data" {
  description = "List of owners with passwords."
  sensitive   = true
  value       = module.db.owners_data
}

output "databases" {
  description = "List of databases names."
  value       = module.db.databases
}

Создаем Redis с помощью модуля https://github.com/terraform-yacloud-modules/terraform-yandex-redis

Hidden text
module "redis" {
  source = "git::https://github.com/terraform-yacloud-modules/terraform-yandex-redis.git?ref=v1.0.0"

  folder_id  = "xxxx"
  name       = "sentry-redis"
  network_id = "xxxxx"
  password   = "sentry-redis-password"
  zone       = "ru-central1-a"
  hosts = {
    host1 = {
      zone      = "ru-central1-a"
      subnet_id = "xxxxx"
    }
  }
}

output "password" {
  value     = module.redis.password
  sensitive = true
}

output "fqdn_redis" {
  value = module.redis.fqdn_redis
}

Создаем S3 с помощью модуля https://github.com/terraform-yacloud-modules/terraform-yandex-storage-bucket

Hidden text
module "s3" {
  source = "git::https://github.com/terraform-yacloud-modules/terraform-yandex-storage-bucket.git?ref=v1.0.0"

  bucket_name = "sentry-bucket-apatsev-dev"
  folder_id   = "xxxx"
}

provider "aws" {
  region                      = "us-east-1"
  skip_region_validation      = true
  skip_credentials_validation = true
  skip_requesting_account_id  = true
  skip_metadata_api_check     = true
  access_key                  = "mock_access_key"
  secret_key                  = "mock_secret_key"
}

output "access_key" {
  value = module.s3.storage_admin_access_key
}

output "secret_key" {
  value     = module.s3.storage_admin_secret_key
  sensitive = true
}

Адреса, креды PostgreSQL, Redis, S3 прописываем в файле values-sentry.yaml

Устанавливаем новое подключение к k8s.

yc managed-kubernetes cluster get-credentials --id xxxx --external

Создаем namespace sentry.

kubectl create namespace sentry

Устанавливаем zookeeper, altinity-clickhouse-operator, strimzi-kafka-operator.
Вместо 3 запусков helm запустим 1 раз helmwave.
Helm репозитории и настройки описаны в файле helmwave.yml

helmwave up --build

Ждем когда поды перейдут в состояние Ready, например через k9s

k9s -A

Создаем kafka-node-pool, kafka, kafka-topics с помощью https://github.com/strimzi/strimzi-kafka-operator.
Примеры берем отсюда: https://github.com/strimzi/strimzi-kafka-operator/tree/main/examples/kafka

kubectl apply -f kafka-node-pool.yaml
kubectl apply -f kafka.yaml
kubectl apply -f kafka-topics.yaml

Ждем, когда поды Kafka перейдут в состояние Ready, например через k9s

k9s -A

Создаем Clickhouse. Придумываем пароль и получаем от него sha256 хеш.

printf 'sentry-clickhouse-password' | sha256sum

Полученный хеш вставляем в поле "sentry/password_sha256_hex" в файле kind-ClickHouseInstallation.yaml

Из примеров: https://github.com/Altinity/clickhouse-operator/tree/master/docs/chi-examples делаем конфиг для clickhouse
Затем применяем его

kubectl apply -f kind-ClickHouseInstallation.yaml

Ждём, когда pod Clickhouse перейдут в состояние Ready, например через k9s

k9s -A

Устанавливаем Sentry.

helm repo add sentry https://sentry-kubernetes.github.io/charts
helm repo update
helm upgrade --install sentry -n sentry sentry/sentry --version 23.1.0 -f values-sentry.yaml

Ждём Clickhouse миграции в pod snuba-migrate.
Чтобы увидеть лог миграции snuba-migrate, можно использовать stern для просмотра логов в namespace sentry.

stern -n sentry -l job-name=sentry-snuba-migrate

Ждём завершения PostgreSQL миграции в pod db-init-job.
Чтобы увидеть лог миграции db-init, можно использовать stern для просмотра логов в namespace sentry.

stern -n sentry -l job-name=sentry-db-init

Смотрим логи в namespace sentry на предмет разных ошибок.

stern -n sentry .

Открываем URL, прописанный в system.url.
Входим в sentry по кредам, которые вы указали в этом коде.

user:
  password: "пароль"
  create: true
  email: логин-в-виде-email

Backend. Пример exception на python.

Создаём Project, выбираем Python. Создаём директорию Example-python. Переходим в директорию Example-python.
В директории example-python создаём main.py такого содержания.

import sentry_sdk
sentry_sdk.init(
    dsn="http://xxxx@sentry.apatsev.org.ru/2",
    traces_sample_rate=1.0,
)

try:
    1 / 0
except ZeroDivisionError:
    sentry_sdk.capture_exception()
python3 -m venv venv
source venv/bin/activate
pip install --upgrade sentry-sdk
python3 main.py

В Sentry видим следующую картину:

Frontend. Пример exception на React.

Вот пример простого React кода для отправки исключения (exception) в Sentry через браузер, а также Dockerfile для контейнеризации этого приложения.

Структура React проекта:

Hidden text
.
├── Dockerfile
├── package.json
├── public
│    └── index.html
└── src
    ├── App.js
    ├── index.css
    └── index.js

App.js:

Hidden text
import React from 'react';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';

// Инициализация Sentry
Sentry.init({
  dsn: 'YOUR_SENTRY_DSN_HERE', // Замените на ваш DSN
  integrations: [new Integrations.BrowserTracing()],
  tracesSampleRate: 1.0,
});

function ErrorButton() {
  function throwError() {
    throw new Error('This is a test error from React');
  }

  return (
    <button onClick={throwError}>
      Throw Error
    </button>
  );
}

function App() {
  return (
    <div className="App">
      <h1>Sentry Example</h1>
      <ErrorButton />
    </div>
  );
}

export default Sentry.withProfiler(App);

index.css:

Hidden text
/* src/index.css */
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

index.js:

Hidden text
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
// Удалите следующую строку, если не используете index.css
// import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

Dockerfile:

Hidden text
# Используем базовый образ Node.js
FROM node:14

# Устанавливаем рабочую директорию
WORKDIR /app

# Копируем package.json и package-lock.json
COPY package*.json ./

# Устанавливаем зависимости
RUN npm install

# Копируем исходный код
COPY . .

# Собираем React приложение
RUN npm run build

# Используем nginx для раздачи статики
FROM nginx:alpine
COPY --from=0 /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Убедитесь, что ваш package.json содержит необходимые зависимости и скрипты для сборки и запуска приложения:
package.json:

Hidden text
{
  "name": "sentry-react-example",
  "version": "1.0.0",
  "description": "Example of sending exceptions to Sentry from React",
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "dependencies": {
    "@sentry/react": "^6.13.3",
    "@sentry/tracing": "^6.13.3",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Сборка и запуск Docker контейнера
Собираем Docker образ:

docker build -t sentry-example .

Запускаем контейнер:

docker run -p 80:80 sentry-example

Не забываем заменить YOUR_SENTRY_DSN_HERE на ваш реальный DSN, который вы можете найти в настройках вашего проекта в Sentry.

В браузере открываем http://localhost

Нажимаем на Throw error

В Sentry видим exception

Mobile. Пример exception на android.

Пробуем запустить https://github.com/sentry-demos/android

В случае ошибок, ответы можно найти в Readme репозитория.

Склонируем репозиторий:

git clone git@github.com:sentry-demos/android.git

Синхронизируем проект с файлами Gradle.

Tools -> Android -> Sync Project with Gradle Files
In some Android Studio version this will be available under:
File -> Sync Project with Gradle Files

Идём в http://sentry.apatsev.org.ru/settings/sentry/auth-tokens/.
Создаем новый token с названием sentry-demos-android:
Идём в Settings -> Auth Token -> Create New token

В корне домашней директории пользователя, кто запускает команду sentry-cli, создаём файл .sentryclirc с содержимым:

[auth]
token=auth-token-sentry-demos-android

В качестве эмулятор android используем Nexus 5x API 29 x86, Pixel 2 API 29.

Создаём Android проект с названием android (по умолчанию).

На вкладке manual копируем io.sentry.dsn.

Меняем значение ключа io.sentry.dsn в файл app/src/main/AndroidManifest.xml

        <meta-data
            android:name="io.sentry.dsn"
            android:value="http://166c996d93bc76f7706c1cf30fcd91af@sentry.apatsev.org.ru/2" />

Помещаем проект и организацию в файл Makefile.

SENTRY_ORG=sentry # Поменяйте на вашу организацию.
SENTRY_PROJECT=android # Поменяйте на ваш проект.

В sentry.properties меняем организацию на Sentry и прописываем Auth Token.

defaults.org=sentry
auth.token=auth-token-sentry-demos-android

Добавляем следующий код в тег <application> нашего AndroidManifest.xml:

<application
...
android:usesCleartextTraffic="true"
...
</application>

Это позволит избежать ошибки java.io.IOException: Cleartext HTTP traffic to sentry.apatsev.org.ru not permitted, которая указывает на то, что приложение пытается отправить данные на сервер Sentry по незашифрованному HTTP-протоколу, что запрещено.

Устанавливаем утилиту https://github.com/getsentry/sentry-cli

Запускаем сборку

make all

Запускаем эмулятор. Нажимаем run app в Android Studio. В самом приложении нажимаем на 3 вертикальные точки.
Затем нажимаем на List App и нажимаем на разные кнопки получая разные Exception.

Исходный код можно скачать в репозитории https://github.com/patsevanton/install-sentry-kubernetes-minimal.

Итак, мы с вами рассмотрели пошаговую инструкцию по минимальной установке Sentry в Kubernetes c clickhouse-operator и kafka-operator, отловы exception на бекенде, в браузере, на Android. А в качестве приятного бонуса, делюсь с вами Telegram-чатом по Sentry https://t.me/sentry_ru

Комментарии (0)