Merge branch 'dev' into add-region-to-organization
This commit is contained in:
commit
369a23551b
94 changed files with 1535 additions and 451 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -1,5 +1,25 @@
|
|||
# Change Log
|
||||
|
||||
## v1.0.50 (2022-11-03)
|
||||
|
||||
- Updates to documentation
|
||||
- Improvements to web schedules
|
||||
- Bug fixes
|
||||
|
||||
## v1.0.49 (2022-11-01)
|
||||
|
||||
- Enable SMTP email backend by default
|
||||
- Fix Grafana sidebar frontend bug
|
||||
|
||||
## v1.0.48 (2022-11-01)
|
||||
|
||||
- verify_number management command
|
||||
- chatops page redesign
|
||||
|
||||
## v1.0.47 (2022-11-01)
|
||||
|
||||
- Bug fixes
|
||||
|
||||
## v1.0.46 (2022-10-28)
|
||||
|
||||
- Bug fixes
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
|
||||
## Developer quickstart
|
||||
|
||||
Related: [How to develop integrations](/engine/config_integrations/README.md)
|
||||
|
||||
### Code style
|
||||
|
||||
- [isort](https://github.com/PyCQA/isort), [black](https://github.com/psf/black) and [flake8](https://github.com/PyCQA/flake8) are used to format backend code
|
||||
|
|
|
|||
|
|
@ -87,5 +87,6 @@ See [Grafana docs](https://grafana.com/docs/grafana/latest/administration/plugin
|
|||
|
||||
- _Migration from the PagerDuty_ - [Migrator](https://github.com/grafana/oncall/tree/dev/tools/pagerduty-migrator)
|
||||
- _Documentation_ - [Grafana OnCall](https://grafana.com/docs/grafana-cloud/oncall/)
|
||||
- _How To Add Integration_ - [How to Add Integration](https://github.com/grafana/oncall/tree/dev/engine/config_integrations/README.md)
|
||||
- _Blog Post_ - [Announcing Grafana OnCall, the easiest way to do on-call management](https://grafana.com/blog/2021/11/09/announcing-grafana-oncall/)
|
||||
- _Presentation_ - [Deep dive into the Grafana, Prometheus, and Alertmanager stack for alerting and on-call management](https://grafana.com/go/observabilitycon/2021/alerting/?pg=blog)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ services:
|
|||
limits:
|
||||
memory: 500m
|
||||
cpus: '0.5'
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
|
|
@ -42,37 +47,27 @@ services:
|
|||
- "15672:15672"
|
||||
- "5672:5672"
|
||||
|
||||
mysql-to-create-grafana-db:
|
||||
image: mysql:5.7
|
||||
platform: linux/x86_64
|
||||
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
restart: always
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: empty
|
||||
MYSQL_DATABASE: grafana
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 500m
|
||||
cpus: '0.5'
|
||||
healthcheck:
|
||||
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
postgres_to_create_grafana_db:
|
||||
image: postgres:14.4
|
||||
command: bash -c "PGPASSWORD=empty psql -U postgres -h postgres -tc \"SELECT 1 FROM pg_database WHERE datname = 'grafana'\" | grep -q 1 || PGPASSWORD=empty psql -U postgres -h postgres -c \"CREATE DATABASE grafana\""
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
grafana:
|
||||
image: "grafana/grafana:main"
|
||||
restart: always
|
||||
environment:
|
||||
GF_DATABASE_TYPE: mysql
|
||||
GF_DATABASE_HOST: mysql
|
||||
GF_DATABASE_USER: root
|
||||
GF_DATABASE_TYPE: postgres
|
||||
GF_DATABASE_HOST: postgres:5432
|
||||
GF_DATABASE_NAME: grafana
|
||||
GF_DATABASE_USER: postgres
|
||||
GF_DATABASE_PASSWORD: empty
|
||||
GF_SECURITY_ADMIN_USER: oncall
|
||||
GF_SECURITY_ADMIN_PASSWORD: oncall
|
||||
GF_DATABASE_SSL_MODE: disable
|
||||
GF_SECURITY_ADMIN_USER: ${GRAFANA_USER:-admin}
|
||||
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
|
||||
GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app
|
||||
GF_INSTALL_PLUGINS: grafana-oncall-app
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
|
@ -83,5 +78,7 @@ services:
|
|||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
mysql-to-create-grafana-db:
|
||||
postgres_to_create_grafana_db:
|
||||
condition: service_completed_successfully
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ pull:
|
|||
|
||||
.PHONY: docs
|
||||
docs: pull
|
||||
docker run -v '$(shell pwd)/sources:$(CONTENT_PATH):Z' -p $(PORT) --rm -it $(IMAGE)
|
||||
docker run -v '$(shell pwd)/sources:$(CONTENT_PATH):Z' -v '$(shell pwd)/sources:/jugo/content/docs/grafana-cloud/oncall:Z' -p $(PORT) --rm -it $(IMAGE)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/
|
||||
- /docs/oncall/latest/
|
||||
canonical: https://grafana.com/docs/oncall/latest/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -11,7 +11,6 @@ keywords:
|
|||
- OnCall
|
||||
- irm
|
||||
title: Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/"
|
||||
weight: 1000
|
||||
---
|
||||
|
||||
|
|
@ -22,7 +21,7 @@ weight: 1000
|
|||
|
||||
Grafana OnCall is an open source incident response management tool built to help teams improve their collaboration and resolve incidents faster. Some of the core strengths of Grafana OnCall include:
|
||||
|
||||
- **Support for a broad set of monitoring systems:** Grafana OnCall supports integrations with many monitoring systems, including Grafana, Prometheus, AlertManager, Zabbix, and more.
|
||||
- **Support for a broad set of monitoring systems:** Grafana OnCall supports integrations with many monitoring systems, including Grafana, Prometheus, Alertmanager, Zabbix, and more.
|
||||
- **Reduce alert noise:** Automatic alert grouping helps avoid alert storms and reduce noise during incidents. Auto-resolve settings can resolve without human intervention when the resolve conditions are met, enabling you to control alert noise and reduce alert fatigue.
|
||||
- **Automatic escalation to on-call rotations:** Grafana OnCall’s flexible calendar integration allows you to define your on-call rotations while managing on-call schedules in your preferred calendar application with iCal format. Configurable alert escalation automatically escalates alerts to on-call team members, notifies slack channels, and more.
|
||||
- **ChatOps focused:** Grafana OnCall integrates closely with your slack workspace to deliver alert notifications to individuals and groups, making daily alerts more visible and easier to manage.
|
||||
|
|
|
|||
20
docs/sources/alert-behavior/_index.md
Normal file
20
docs/sources/alert-behavior/_index.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/alert-behavior/
|
||||
canonical: https://grafana.com/docs/oncall/latest/alert-behavior/
|
||||
title: Configure alert behavior for Grafana OnCall
|
||||
weight: 900
|
||||
---
|
||||
|
||||
# Configure alert behavior for Grafana OnCall
|
||||
|
||||
The available alert configurations in Grafana OnCall allow you to define how certain alerts are handled and ensure that alerts are routed, escalated, and grouped to fit your specific alerting needs. Grafana OnCall can receive alerts from any monitoring system that sends alerts via webhook.
|
||||
|
||||
|
||||
## About alert behavior
|
||||
|
||||
Once Grafana OnCall receives an alert, the following occurs, based on the alert content:
|
||||
|
||||
- Default or customized alert templates are applied to deliver the most useful alert fields with the most valuable information, in a readable format.
|
||||
- Alerts are grouped based on your alert grouping configurations, combining similar or related alerts to reduce alert noise.
|
||||
- Alerts automatically resolve if an alert from the monitoring system matches the resolve condition for that alert.
|
||||
|
|
@ -1,18 +1,19 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/integrations/create-custom-templates/
|
||||
- ../integrations/create-custom-templates/
|
||||
- /docs/oncall/latest/alert-behavior/alert-templates/
|
||||
canonical: https://grafana.com/docs/oncall/latest/alert-behavior/alert-templates/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
- Notifications
|
||||
- on-call
|
||||
- Jinja
|
||||
title: Configure alerts in Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/create-custom-templates/"
|
||||
title: Configure alert templates
|
||||
weight: 300
|
||||
---
|
||||
|
||||
# Configure alerts in Grafana OnCall
|
||||
# Configure alert templates
|
||||
|
||||
Grafana OnCall can integrate with any monitoring systems that can send alerts using webhooks with JSON payloads. By default, webhooks deliver raw JSON payloads. When Grafana OnCall receives an alert and parses its payload, a default pre configured alert template is applied to modify the alert payload to be more human readable. These alert templates are customizable for any integration.
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/integrations/configure-outgoing-webhooks/
|
||||
- ../integrations/configure-outgoing-webhooks/
|
||||
- /docs/oncall/latest/alert-behavior/outgoing-webhooks/
|
||||
canonical: https://grafana.com/docs/oncall/latest/alert-behavior/outgoing-webhooks/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -9,7 +11,6 @@ keywords:
|
|||
- amixr
|
||||
- webhooks
|
||||
title: Configure outgoing webhooks for Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/configure-outgoing-webhooks/"
|
||||
weight: 500
|
||||
---
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/calendar-schedules/
|
||||
canonical: https://grafana.com/docs/oncall/latest/calendar-schedules/
|
||||
description: ""
|
||||
keywords:
|
||||
- Grafana
|
||||
|
|
@ -8,7 +9,6 @@ keywords:
|
|||
- on-call
|
||||
- calendar
|
||||
title: Configure and manage on-call schedules
|
||||
canonical: "https://grafana.com/docs/oncall/latest/calendar-schedules/"
|
||||
weight: 1100
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/configure-user-settings/
|
||||
canonical: https://grafana.com/docs/oncall/latest/configure-user-setting/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -10,7 +11,6 @@ keywords:
|
|||
- oncall
|
||||
- integrations
|
||||
title: Manage users and teams for Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/configure-user-setting/"
|
||||
weight: 1300
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
---
|
||||
title: Configure escalation chains and routes for Grafana OnCall
|
||||
weight: 700
|
||||
canonical: "https://grafana.com/docs/oncall/latest/escalation-policies/"
|
||||
aliases:
|
||||
- /docs/oncall/latest/chat-options/escalation-policies/
|
||||
- /docs/oncall/latest/escalation-policies/
|
||||
canonical: https://grafana.com/docs/oncall/latest/escalation-policies/
|
||||
title: Escalation Chains and Routes
|
||||
weight: 700
|
||||
---
|
||||
|
||||
|
||||
# Configure escalation chains and routes for Grafana OnCall
|
||||
# Escalation Chains and Routes
|
||||
|
||||
Escalation chains and routes for Grafana OnCall
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/escalation-policies/configure-escalation-chains/
|
||||
canonical: https://grafana.com/docs/oncall/latest/escalation-policies/configure-escalation-chains/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -10,7 +11,6 @@ keywords:
|
|||
- oncall
|
||||
- integrations
|
||||
title: Configure and manage Escalation Chains
|
||||
canonical: "https://grafana.com/docs/oncall/latest/escalation-policies/configure-escalation-chains/"
|
||||
weight: 100
|
||||
---
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/escalation-policies/configure-routes/
|
||||
canonical: https://grafana.com/docs/oncall/latest/escalation-policies/configure-routes/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -10,11 +11,10 @@ keywords:
|
|||
- oncall
|
||||
- integrations
|
||||
title: Configure and manage routes
|
||||
canonical: "https://grafana.com/docs/oncall/latest/escalation-policies/configure-routes/"
|
||||
weight: 300
|
||||
---
|
||||
|
||||
# Configure and manage routes
|
||||
# Configure and manage Routes
|
||||
|
||||
Set up escalation chains and routes to configure escalation behavior for alert group notifications.
|
||||
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/getting-started/
|
||||
- /docs/oncall/latest/getting-started/
|
||||
- /docs/oncall/latest/get-started/
|
||||
- /getting-started/
|
||||
canonical: https://grafana.com/docs/oncall/latest/get-started/
|
||||
keywords:
|
||||
- Get started
|
||||
- On call
|
||||
- Grafana Cloud
|
||||
title: Get started with Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/getting-started/"
|
||||
weight: 300
|
||||
---
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ Regardless of where your alerts originate, you can send them to Grafana OnCall v
|
|||
2. Explore the alert by clicking on the title of the alert.
|
||||
3. Acknowledge and resolve the test alert.
|
||||
|
||||
For more information on Grafana OnCall integrations and further configuration guidance, refer to, [Connect to Grafana OnCall]({{< relref "../integrations" >}})
|
||||
For more information on Grafana OnCall integrations and further configuration guidance, refer to, [Grafana OnCall integrations]({{< relref "../integrations" >}})
|
||||
|
||||
|
||||
### Configure Escalation Chains
|
||||
|
|
@ -105,7 +105,7 @@ To configure Slack for Grafana OnCall:
|
|||
5. Click Allow to allow Grafana OnCall to access Slack.
|
||||
6. Ensure users verify their Slack accounts in their user profile in Grafana OnCall.
|
||||
|
||||
For further instruction on connecting to your Slack workspace, refer to [Connect Slack to Grafana OnCall]({{< relref "../chat-options/configure-slack" >}})
|
||||
For further instruction on connecting to your Slack workspace, refer to [Slack integration for Grafana OnCall]({{< relref "../integrations/chatops-integrations/configure-slack/" >}})
|
||||
|
||||
|
||||
### Add your on-call schedule
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/integrations/
|
||||
- /docs/oncall/latest/integrations/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -10,12 +10,11 @@ keywords:
|
|||
- amixr
|
||||
- oncall
|
||||
- integrations
|
||||
title: Connect to Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/"
|
||||
title: Grafana OnCall integrations
|
||||
weight: 500
|
||||
---
|
||||
|
||||
# Connect to Grafana OnCall
|
||||
# Grafana OnCall integrations
|
||||
|
||||
Integrations allow you to connect monitoring systems of your choice to send alerts to Grafana OnCall. Regardless of where your alerts originate, you can configure alerts to be sent to Grafana OnCall for alert escalation and notification. Grafana OnCall receives alerts in JSON format via a POST request, OnCall then parses alert data using preconfigured alert templates to determine alert grouping, apply routes, and determine correct escalation.
|
||||
|
||||
|
|
@ -53,7 +52,7 @@ To customize alert grouping for an integration:
|
|||
2. Select **Alert Behavior** from the dropdown menu next to **Edit template for**.
|
||||
3. Edit the **grouping id**, **acknowledge condition**, and **resolve condition** templates as needed to customize your alert behavior.
|
||||
|
||||
For more information on alert templates, see [Configure alerts in Grafana OnCall]({{< relref "create-custom-templates/" >}})
|
||||
For more information on alert templates, see [Configure alerts templates]({{< relref "../alert-behavior/alert-templates" >}})
|
||||
|
||||
#### Add Routes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/integrations/add-integration/
|
||||
- /docs/oncall/latest/integrations/available-integrations/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -10,15 +10,14 @@ keywords:
|
|||
- Alertmanager
|
||||
- Prometheus
|
||||
title: Currently available integrations for Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/available-integrations/"
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# Currently available integrations
|
||||
# Available integrations
|
||||
|
||||
Grafana OnCall can connect directly to the monitoring services where your alerts originate. All currently available integrations are listed in the Grafana OnCall **Create Integration** section.
|
||||
|
||||
If the integration you're looking for isn't currently listed, see [Configure Webhook integrations for Grafana OnCall]({{< relref "../add-webhook-integration/" >}}) to integration your monitoring system with Grafana OnCall.
|
||||
If the integration you're looking for isn't currently listed, see [Webhook integrations for Grafana OnCall]({{< relref "../available-integrations/configure-webhook" >}}) to integration your monitoring system with Grafana OnCall.
|
||||
|
||||
> **Note:** Some integrations are available for Grafana Cloud instances only. See individual integration guides for more information.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/available-integrations/add-alertmanager/
|
||||
- /docs/oncall/latest/integrations/available-integrations /add-alertmanager/
|
||||
- add-alertmanager/
|
||||
- /docs/oncall/latest/integrations/available-integrations/configure-alertmanager/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-alertmanager/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -9,12 +10,11 @@ keywords:
|
|||
- on-call
|
||||
- Alertmanager
|
||||
- Prometheus
|
||||
title: Connect Alertmanager to Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/available-integrations/add-alertmanager/"
|
||||
title: Alertmanager integration for Grafana OnCall
|
||||
weight: 300
|
||||
---
|
||||
|
||||
# Connect Alertmanager to Grafana OnCall
|
||||
# Alertmanager integration for Grafana OnCall
|
||||
|
||||
The Alertmanager integration for Grafana OnCall handles alerts sent by client applications such as the Prometheus server.
|
||||
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/integrations/add-grafana-alerting/
|
||||
- /docs/oncall/latest/integrations/available-integrations /add-grafana-alerting/
|
||||
- add-grafana-alerting/
|
||||
- /docs/oncall/latest/integrations/available-integrations/configure-grafana-alerting/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-grafana-alerting/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
- Notifications
|
||||
- on-call
|
||||
- Prometheus
|
||||
title: Connect Grafana Alerting to Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/available-integrations/add-grafana-alerting/"
|
||||
title: Grafana Alerting integration for Grafana OnCall
|
||||
weight: 100
|
||||
---
|
||||
|
||||
# Connect Grafana Alerting to Grafana OnCall
|
||||
# Grafana Alerting integration for Grafana OnCall
|
||||
|
||||
Grafana Alerting for Grafana OnCall can be set up using two methods:
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/integrations/add-webhook-integration/
|
||||
- ../add-webhook-integration/
|
||||
- /docs/oncall/latest/integrations/available-integrations/configure-webhook/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-webhook/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -9,11 +11,10 @@ keywords:
|
|||
- Alertmanager
|
||||
- Prometheus
|
||||
title: Webhook integration for Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/add-webhook-integration/"
|
||||
weight: 700
|
||||
---
|
||||
|
||||
# Configure Webhook integrations for Grafana OnCall
|
||||
# Webhook integrations for Grafana OnCall
|
||||
|
||||
Grafana OnCall directly supports many integrations, those that aren’t currently listed in the Integrations menu can be connected using the webhook integration and configured alert templates.
|
||||
|
||||
|
|
@ -56,4 +57,4 @@ For example:
|
|||
}'
|
||||
```
|
||||
|
||||
To learn how to use custom alert templates for formatted webhooks, see [Configure alerts in Grafana OnCall]({{< relref "../integrations/create-custom-templates/" >}}).
|
||||
To learn how to use custom alert templates for formatted webhooks, see [Configure alerts templates]({{< relref "../../../alert-behavior/alert-templates/" >}}).
|
||||
|
|
@ -1,18 +1,19 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/integrations/available-integrations /add-zabbix/
|
||||
- add-zabbix/
|
||||
- /docs/oncall/latest/integrations/available-integrations/configure-zabbix/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-zabbix/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
- Notifications
|
||||
- on-call
|
||||
- Zabbix
|
||||
title: Connect Zabbix to Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/integrations/available-integrations/add-zabbix/"
|
||||
title: Zabbix integration for Grafana OnCall
|
||||
weight: 500
|
||||
---
|
||||
|
||||
# Connect Zabbix to Grafana OnCall
|
||||
# Zabbix integration for Grafana OnCall
|
||||
|
||||
Zabbix is an open-source monitoring software tool for diverse IT components, including networks, servers, virtual machines, and cloud services. Zabbix provides monitoring for metrics such as network utilization, CPU load, and disk space consumption.
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/chat-options/
|
||||
- ../chat-options/
|
||||
- /docs/oncall/latest/integrations/chatops-integrations/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -9,14 +11,13 @@ keywords:
|
|||
- amixr
|
||||
- oncall
|
||||
- slack
|
||||
title: Connect ChatOps to Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/chat-options/"
|
||||
weight: 900
|
||||
title: Available ChatOps integrations
|
||||
weight: 300
|
||||
---
|
||||
|
||||
# Connect ChatOps to Grafana OnCall
|
||||
# Available ChatOps integrations
|
||||
|
||||
Grafana OnCall directly supports the export of alert notifications to some popular messaging applications like Slack and Telegram. You can use outgoing webhooks to applications that aren't directly supported. For information on configuring outgoing webhooks, see [Send alert group notifications by webhook]({{< relref "../integrations/configure-outgoing-webhooks.md" >}}).
|
||||
Grafana OnCall directly supports the export of alert notifications to some popular messaging applications like Slack and Telegram. You can use outgoing webhooks to applications that aren't directly supported. For information on configuring outgoing webhooks, see [Send alert group notifications by webhook]({{< relref "../../alert-behavior/outgoing-webhooks/" >}}).
|
||||
|
||||
To configure supported messaging apps, see the following topics:
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/chat-options/configure-slack/
|
||||
- ../../chat-options/configure-slack/
|
||||
- /docs/oncall/latest/integrations/chatops-integrations/configure-slack/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-slack/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -10,7 +12,6 @@ keywords:
|
|||
- oncall
|
||||
- slack
|
||||
title: Slack integration for Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/chat-options/configure-slack/"
|
||||
weight: 100
|
||||
---
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ Integrating your Slack workspace with Grafana OnCall allows users and teams to b
|
|||
|
||||
To install the Slack integration, you must have Admin permissions in your Grafana instance as well as the Slack workspace that you’d like to integrate with.
|
||||
|
||||
For Open Source Grafana OnCall Slack installation guidance, refer to [Open Source Grafana OnCall]({{< relref "../open-source" >}}).
|
||||
For Open Source Grafana OnCall Slack installation guidance, refer to [Open Source Grafana OnCall]({{< relref "../../../open-source/" >}}).
|
||||
|
||||
## Install Slack integration for Grafana OnCall
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/chat-options/configure-teams/
|
||||
- ../../chat-options/configure-teams/
|
||||
- /docs/oncall/latest/integrations/chatops-integrations/configure-teams/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-teams/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -11,7 +13,6 @@ keywords:
|
|||
- MS Team
|
||||
- Microsoft
|
||||
title: Microsoft Teams integration for Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/chat-options/configure-teams/"
|
||||
weight: 500
|
||||
---
|
||||
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/chat-options/configure-telegram/
|
||||
- ../../chat-options/configure-telegram/
|
||||
- /docs/oncall/latest/integrations/chatops-integrations/configure-telegram/
|
||||
canonical: https://grafana.com/docs/oncall/latest/integrations/chatops-integrations/configure-telegram/
|
||||
keywords:
|
||||
- Grafana Cloud
|
||||
- Alerts
|
||||
|
|
@ -10,7 +12,6 @@ keywords:
|
|||
- oncall
|
||||
- telegram
|
||||
title: Telegram integration for Grafana OnCall
|
||||
canonical: "https://grafana.com/docs/oncall/latest/chat-options/configure-telegram/"
|
||||
weight: 300
|
||||
---
|
||||
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/oncall/latest/oncall-api-reference/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/
|
||||
title: Grafana OnCall HTTP API reference
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/"
|
||||
weight: 1500
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/alertgroups/
|
||||
- /docs/oncall/latest/oncall-api-reference/alertgroups/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/
|
||||
title: Alert groups HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/alertgroups/"
|
||||
weight: 400
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/alerts/
|
||||
- /docs/oncall/latest/oncall-api-reference/alerts/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/alerts/
|
||||
title: Alerts HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/alerts/"
|
||||
weight: 100
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/escalation_chains/
|
||||
- /docs/oncall/latest/oncall-api-reference/escalation_chains/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_chains/
|
||||
title: Escalation Chains HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_chains/"
|
||||
weight: 200
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/escalation_policies/
|
||||
- /docs/oncall/latest/oncall-api-reference/escalation_policies/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_policies/
|
||||
title: Escalation Policies HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/escalation_policies/"
|
||||
weight: 300
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/integrations/
|
||||
- /docs/oncall/latest/oncall-api-reference/integrations/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/integrations/
|
||||
title: Integrations HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/integrations/"
|
||||
weight: 500
|
||||
---
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ The above command returns JSON structured in the following way:
|
|||
```
|
||||
|
||||
Integrations are sources of alerts and alert groups for Grafana OnCall.
|
||||
For example, to learn how to integrate Grafana OnCall with Alertmanager see [Alertmanager]({{< relref "../integrations/available-integrations/add-alertmanager/" >}}).
|
||||
For example, to learn how to integrate Grafana OnCall with Alertmanager see [Alertmanager]({{< relref "../integrations/available-integrations/configure-alertmanager/" >}}).
|
||||
|
||||
**HTTP request**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/on_call_shifts/
|
||||
- /docs/oncall/latest/oncall-api-reference/on_call_shifts/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/on_call_shifts/
|
||||
title: OnCall shifts HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/on_call_shifts/"
|
||||
weight: 600
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/outgoing_webhooks/
|
||||
- /docs/oncall/latest/oncall-api-reference/outgoing_webhooks/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/
|
||||
title: Outgoing webhooks HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/"
|
||||
weight: 700
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/personal_notification_rules/
|
||||
- /docs/oncall/latest/oncall-api-reference/personal_notification_rules/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/
|
||||
title: Personal Notification Rules HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/personal_notification_rules/"
|
||||
weight: 800
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/postmortem_messages/
|
||||
- /docs/oncall/latest/oncall-api-reference/postmortem_messages/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortem_messages/
|
||||
draft: true
|
||||
title: Postmortem Messages HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortem_messages/"
|
||||
weight: 900
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/postmortems/
|
||||
- /docs/oncall/latest/oncall-api-reference/postmortems/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortems/
|
||||
draft: true
|
||||
title: Postmortem HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/postmortems/"
|
||||
weight: 1000
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/routes/
|
||||
- /docs/oncall/latest/oncall-api-reference/routes/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/routes/
|
||||
title: Routes HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/routes/"
|
||||
weight: 1100
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/schedules/
|
||||
- /docs/oncall/latest/oncall-api-reference/schedules/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/schedules/
|
||||
title: Schedule HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/schedules/"
|
||||
weight: 1200
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/slack_channels/
|
||||
- /docs/oncall/latest/oncall-api-reference/slack_channels/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/slack_channels/
|
||||
title: Slack Channels HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/slack_channels/"
|
||||
weight: 1300
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/user_groups/
|
||||
- /docs/oncall/latest/oncall-api-reference/user_groups/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/user_groups/
|
||||
title: OnCall User Groups HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/user_groups/"
|
||||
weight: 1400
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/oncall-api-reference/users/
|
||||
- /docs/oncall/latest/oncall-api-reference/users/
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/users/
|
||||
title: Grafana OnCall Users HTTP API
|
||||
canonical: "https://grafana.com/docs/oncall/latest/oncall-api-reference/users/"
|
||||
weight: 1500
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
aliases:
|
||||
- /docs/grafana-cloud/oncall/open-source/
|
||||
- /docs/oncall/latest/open-source/
|
||||
keywords:
|
||||
- Open Source
|
||||
|
|
@ -195,6 +194,7 @@ Grafana OnCall is capable of sending emails using SMTP as a user notification st
|
|||
- `EMAIL_HOST_USER` - SMTP server user
|
||||
- `EMAIL_HOST_PASSWORD` - SMTP server password
|
||||
- `EMAIL_PORT` (default is `587`) - SMTP server port
|
||||
- `EMAIL_USE_TLS` (default is `True`) - to enable/disable TLS
|
||||
- `EMAIL_USE_TLS` (default is `True`) - To enable/disable TLS
|
||||
- `EMAIL_FROM_ADDRESS` (optional) - Email address used to send emails. If not specified, `EMAIL_HOST_USER` will be used.
|
||||
|
||||
After enabling the email integration, it will be possible to use the `Notify by email` notification step in user settings.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from apps.base.models import LiveSetting
|
|||
|
||||
class LiveSettingSerializer(serializers.ModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
value = serializers.JSONField(allow_null=False)
|
||||
value = serializers.JSONField(allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = LiveSetting
|
||||
|
|
|
|||
|
|
@ -205,11 +205,11 @@ def test_get_list_schedules_by_type(
|
|||
},
|
||||
]
|
||||
|
||||
for expected, schedule_type in enumerate(("api", "ical", "web")):
|
||||
for schedule_type in range(3):
|
||||
url = reverse("api-internal:schedule-list") + "?type={}".format(schedule_type)
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == [expected_payload[expected]]
|
||||
assert response.json() == [expected_payload[schedule_type]]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from apps.api.serializers.schedule_polymorphic import (
|
|||
from apps.auth_token.auth import PluginAuthentication
|
||||
from apps.auth_token.constants import SCHEDULE_EXPORT_TOKEN_NAME
|
||||
from apps.auth_token.models import ScheduleExportAuthToken
|
||||
from apps.schedules.models import OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleICal, OnCallScheduleWeb
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
from apps.slack.models import SlackChannel
|
||||
from apps.slack.tasks import update_slack_user_group_for_schedules
|
||||
from common.api_helpers.exceptions import BadRequest, Conflict
|
||||
|
|
@ -42,7 +42,9 @@ EVENTS_FILTER_BY_ROTATION = "rotation"
|
|||
EVENTS_FILTER_BY_OVERRIDE = "override"
|
||||
EVENTS_FILTER_BY_FINAL = "final"
|
||||
|
||||
SCHEDULE_TYPE_TO_CLASS = {"api": OnCallScheduleCalendar, "ical": OnCallScheduleICal, "web": OnCallScheduleWeb}
|
||||
SCHEDULE_TYPE_TO_CLASS = {
|
||||
str(num_type): cls for cls, num_type in PolymorphicScheduleSerializer.SCHEDULE_CLASS_TO_TYPE.items()
|
||||
}
|
||||
|
||||
|
||||
class ScheduleView(
|
||||
|
|
@ -262,7 +264,13 @@ class ScheduleView(
|
|||
if filter_by is not None and filter_by != EVENTS_FILTER_BY_FINAL:
|
||||
filter_by = OnCallSchedule.PRIMARY if filter_by == EVENTS_FILTER_BY_ROTATION else OnCallSchedule.OVERRIDES
|
||||
events = schedule.filter_events(
|
||||
user_tz, starting_date, days=days, with_empty=True, with_gap=resolve_schedule, filter_by=filter_by
|
||||
user_tz,
|
||||
starting_date,
|
||||
days=days,
|
||||
with_empty=True,
|
||||
with_gap=resolve_schedule,
|
||||
filter_by=filter_by,
|
||||
all_day_datetime=True,
|
||||
)
|
||||
else: # return final schedule
|
||||
events = schedule.final_events(user_tz, starting_date, days)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class LiveSetting(models.Model):
|
|||
"EMAIL_HOST_USER",
|
||||
"EMAIL_HOST_PASSWORD",
|
||||
"EMAIL_USE_TLS",
|
||||
"EMAIL_FROM_ADDRESS",
|
||||
"TWILIO_ACCOUNT_SID",
|
||||
"TWILIO_AUTH_TOKEN",
|
||||
"TWILIO_NUMBER",
|
||||
|
|
@ -61,6 +62,7 @@ class LiveSetting(models.Model):
|
|||
"EMAIL_HOST_USER": "SMTP server user",
|
||||
"EMAIL_HOST_PASSWORD": "SMTP server password",
|
||||
"EMAIL_USE_TLS": "SMTP enable/disable TLS",
|
||||
"EMAIL_FROM_ADDRESS": "Email address used to send emails. If not specified, EMAIL_HOST_USER will be used.",
|
||||
"SLACK_SIGNING_SECRET": (
|
||||
"Check <a href='"
|
||||
"https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,16 @@ MAX_RETRIES = 1 if settings.DEBUG else 10
|
|||
logger = get_task_logger(__name__)
|
||||
|
||||
|
||||
def get_from_email(user):
|
||||
if live_settings.EMAIL_FROM_ADDRESS:
|
||||
return live_settings.EMAIL_FROM_ADDRESS
|
||||
|
||||
if settings.LICENSE == settings.CLOUD_LICENSE_NAME:
|
||||
return "oncall@{}.grafana.net".format(user.organization.stack_slug)
|
||||
|
||||
return live_settings.EMAIL_HOST_USER
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=MAX_RETRIES)
|
||||
def notify_user_async(user_pk, alert_group_pk, notification_policy_pk):
|
||||
# imported here to avoid circular import error
|
||||
|
|
@ -39,6 +49,20 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk):
|
|||
logger.warning(f"User notification policy {notification_policy_pk} does not exist")
|
||||
return
|
||||
|
||||
# create an error log in case EMAIL_HOST is not specified
|
||||
if not live_settings.EMAIL_HOST:
|
||||
UserNotificationPolicyLogRecord.objects.create(
|
||||
author=user,
|
||||
type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED,
|
||||
notification_policy=notification_policy,
|
||||
alert_group=alert_group,
|
||||
reason="Error while sending email",
|
||||
notification_step=notification_policy.step,
|
||||
notification_channel=notification_policy.notify_by,
|
||||
)
|
||||
logger.error(f"Error while sending email: empty EMAIL_HOST env variable")
|
||||
return
|
||||
|
||||
emails_left = user.organization.emails_left(user)
|
||||
if emails_left <= 0:
|
||||
UserNotificationPolicyLogRecord.objects.create(
|
||||
|
|
@ -62,7 +86,7 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk):
|
|||
subject, html_message = build_subject_and_message(alert_group, emails_left)
|
||||
|
||||
message = strip_tags(html_message)
|
||||
email_from = settings.EMAIL_HOST_USER
|
||||
from_email = get_from_email(user)
|
||||
recipient_list = [user.email]
|
||||
|
||||
connection = get_connection(
|
||||
|
|
@ -76,7 +100,7 @@ def notify_user_async(user_pk, alert_group_pk, notification_policy_pk):
|
|||
)
|
||||
|
||||
try:
|
||||
send_mail(subject, message, email_from, recipient_list, html_message=html_message, connection=connection)
|
||||
send_mail(subject, message, from_email, recipient_list, html_message=html_message, connection=connection)
|
||||
EmailMessage.objects.create(
|
||||
represents_alert_group=alert_group,
|
||||
notification_policy=notification_policy,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from django.core import mail
|
|||
from django.core.mail.backends.locmem import EmailBackend
|
||||
|
||||
from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord
|
||||
from apps.email.tasks import notify_user_async
|
||||
from apps.email.tasks import get_from_email, notify_user_async
|
||||
from apps.user_management.subscription_strategy.free_public_beta_subscription_strategy import (
|
||||
FreePublicBetaSubscriptionStrategy,
|
||||
)
|
||||
|
|
@ -24,6 +24,7 @@ def test_notify_user(
|
|||
make_user_notification_policy,
|
||||
):
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
settings.EMAIL_HOST = "test"
|
||||
|
||||
organization = make_organization()
|
||||
user = make_user_for_organization(organization)
|
||||
|
|
@ -44,6 +45,42 @@ def test_notify_user(
|
|||
assert len(mail.outbox) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notify_empty_email_host(
|
||||
settings,
|
||||
make_organization,
|
||||
make_user_for_organization,
|
||||
make_token_for_organization,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
make_user_notification_policy,
|
||||
):
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
settings.EMAIL_HOST = None
|
||||
|
||||
organization = make_organization()
|
||||
user = make_user_for_organization(organization)
|
||||
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
|
||||
make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload)
|
||||
|
||||
notification_policy = make_user_notification_policy(
|
||||
user,
|
||||
UserNotificationPolicy.Step.NOTIFY,
|
||||
notify_by=8,
|
||||
important=False,
|
||||
)
|
||||
|
||||
notify_user_async(user.pk, alert_group.pk, notification_policy.pk)
|
||||
assert len(mail.outbox) == 0
|
||||
|
||||
log_record = notification_policy.personal_log_records.last()
|
||||
assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_notify_user_bad_smtp_host(
|
||||
settings,
|
||||
|
|
@ -56,6 +93,7 @@ def test_notify_user_bad_smtp_host(
|
|||
make_user_notification_policy,
|
||||
):
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
settings.EMAIL_HOST = "test"
|
||||
|
||||
organization = make_organization()
|
||||
user = make_user_for_organization(organization)
|
||||
|
|
@ -93,6 +131,7 @@ def test_notify_user_no_emails_left(
|
|||
make_user_notification_policy,
|
||||
):
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
settings.EMAIL_HOST = "test"
|
||||
|
||||
organization = make_organization()
|
||||
user = make_user_for_organization(organization)
|
||||
|
|
@ -116,3 +155,38 @@ def test_notify_user_no_emails_left(
|
|||
log_record = notification_policy.personal_log_records.last()
|
||||
assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
assert log_record.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"license_name,email_host_user,email_from_address,expected",
|
||||
[
|
||||
("Cloud", "user", "from_address", "from_address"),
|
||||
("OpenSource", "user", "from_address", "from_address"),
|
||||
("Cloud", "user", None, "oncall@slug.grafana.net"),
|
||||
("Cloud", None, None, "oncall@slug.grafana.net"),
|
||||
("OpenSource", "user", None, "user"),
|
||||
("OpenSource", None, None, None),
|
||||
],
|
||||
)
|
||||
def test_get_from_email(
|
||||
settings,
|
||||
make_organization,
|
||||
make_user_for_organization,
|
||||
license_name,
|
||||
email_host_user,
|
||||
email_from_address,
|
||||
expected,
|
||||
):
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
settings.EMAIL_HOST = "test"
|
||||
settings.EMAIL_HOST_PASSWORD = "password"
|
||||
|
||||
settings.LICENSE = license_name
|
||||
settings.EMAIL_HOST_USER = email_host_user
|
||||
settings.EMAIL_FROM_ADDRESS = email_from_address
|
||||
|
||||
organization = make_organization(stack_slug="slug")
|
||||
user = make_user_for_organization(organization)
|
||||
|
||||
assert get_from_email(user) == expected
|
||||
|
|
|
|||
|
|
@ -202,7 +202,16 @@ class OnCallSchedule(PolymorphicModel):
|
|||
"""Return public primary keys for all users referenced in the schedule."""
|
||||
return set()
|
||||
|
||||
def filter_events(self, user_timezone, starting_date, days, with_empty=False, with_gap=False, filter_by=None):
|
||||
def filter_events(
|
||||
self,
|
||||
user_timezone,
|
||||
starting_date,
|
||||
days,
|
||||
with_empty=False,
|
||||
with_gap=False,
|
||||
filter_by=None,
|
||||
all_day_datetime=False,
|
||||
):
|
||||
"""Return filtered events from schedule."""
|
||||
shifts = (
|
||||
list_of_oncall_shifts_from_ical(
|
||||
|
|
@ -212,13 +221,18 @@ class OnCallSchedule(PolymorphicModel):
|
|||
)
|
||||
events = []
|
||||
for shift in shifts:
|
||||
all_day = type(shift["start"]) == datetime.date
|
||||
start = shift["start"]
|
||||
all_day = type(start) == datetime.date
|
||||
# fix confusing end date for all-day event
|
||||
end = shift["end"] - timezone.timedelta(days=1) if all_day else shift["end"]
|
||||
if all_day and all_day_datetime:
|
||||
start = datetime.datetime.combine(start, datetime.datetime.min.time(), tzinfo=pytz.UTC)
|
||||
end = datetime.datetime.combine(end, datetime.datetime.max.time(), tzinfo=pytz.UTC)
|
||||
is_gap = shift.get("is_gap", False)
|
||||
shift_json = {
|
||||
"all_day": all_day,
|
||||
"start": shift["start"],
|
||||
# fix confusing end date for all-day event
|
||||
"end": shift["end"] - timezone.timedelta(days=1) if all_day else shift["end"],
|
||||
"start": start,
|
||||
"end": end,
|
||||
"users": [
|
||||
{
|
||||
"display_name": user.username,
|
||||
|
|
@ -246,7 +260,9 @@ class OnCallSchedule(PolymorphicModel):
|
|||
|
||||
def final_events(self, user_tz, starting_date, days):
|
||||
"""Return schedule final events, after resolving shifts and overrides."""
|
||||
events = self.filter_events(user_tz, starting_date, days=days, with_empty=True, with_gap=True)
|
||||
events = self.filter_events(
|
||||
user_tz, starting_date, days=days, with_empty=True, with_gap=True, all_day_datetime=True
|
||||
)
|
||||
events = self._resolve_schedule(events)
|
||||
return events
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,20 @@ SUMMARY:@Alex
|
|||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;VALUE=DATE:20210127
|
||||
DTEND;VALUE=DATE:20210129
|
||||
DTSTAMP:20210127T154139Z
|
||||
UID:7q00jpu4hdlr9e3j4fftbv7kt8@google.com
|
||||
CREATED:20210127T143802Z
|
||||
DESCRIPTION:
|
||||
LAST-MODIFIED:20210127T143802Z
|
||||
LOCATION:
|
||||
SEQUENCE:0
|
||||
STATUS:CONFIRMED
|
||||
SUMMARY:@Alice
|
||||
TRANSP:TRANSPARENT
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
DTSTART;TZID=Asia/Yekaterinburg:20210127T130000
|
||||
DTEND;TZID=Asia/Yekaterinburg:20210127T220000
|
||||
DTSTAMP:20210127T154139Z
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ def test_recurring_ical_events_with_all_day_event(get_ical):
|
|||
parsed_iso_day_to_check - timezone.timedelta(days=1),
|
||||
parsed_iso_day_to_check + timezone.timedelta(days=1),
|
||||
)
|
||||
assert len(events) == 4
|
||||
assert len(events) == 5
|
||||
assert events[0]["SUMMARY"] == "@Alex"
|
||||
assert events[1]["SUMMARY"] == "@Bob"
|
||||
assert events[2]["SUMMARY"] == "@Bernard Desruisseaux"
|
||||
assert events[1]["SUMMARY"] == "@Alice"
|
||||
assert events[2]["SUMMARY"] == "@Bob"
|
||||
assert events[3]["SUMMARY"] == "@Bernard Desruisseaux"
|
||||
assert events[4]["SUMMARY"] == "@Bernard Desruisseaux"
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ def test_shifts_dict_all_day_middle_event(make_organization, make_schedule, get_
|
|||
parsed_iso_day_to_check = datetime.datetime.fromisoformat(day_to_check_iso).replace(tzinfo=pytz.UTC)
|
||||
requested_date = (parsed_iso_day_to_check - timezone.timedelta(days=1)).date()
|
||||
shifts = list_of_oncall_shifts_from_ical(schedule, requested_date, days=3, with_empty_shifts=True)
|
||||
assert len(shifts) == 4
|
||||
assert len(shifts) == 5
|
||||
for s in shifts:
|
||||
start = s["start"].date() if isinstance(s["start"], datetime.datetime) else s["start"]
|
||||
end = s["end"].date() if isinstance(s["end"], datetime.datetime) else s["end"]
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ def test_filter_events_ical_all_day(make_organization, make_user_for_organizatio
|
|||
organization = make_organization()
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar)
|
||||
schedule.cached_ical_file_primary = calendar.to_ical()
|
||||
for u in ("@Bernard Desruisseaux", "@Bob", "@Alex"):
|
||||
for u in ("@Bernard Desruisseaux", "@Bob", "@Alex", "@Alice"):
|
||||
make_user_for_organization(organization, username=u)
|
||||
# clear users pks <-> organization cache (persisting between tests)
|
||||
memoized_users_in_ical.cache_clear()
|
||||
|
|
@ -246,14 +246,44 @@ def test_filter_events_ical_all_day(make_organization, make_user_for_organizatio
|
|||
|
||||
events = schedule.final_events("UTC", start_date, days=2)
|
||||
expected_events = [
|
||||
# all_day, users, start
|
||||
(False, ["@Bernard Desruisseaux"], datetime.datetime(2021, 1, 26, 8, 0, tzinfo=pytz.UTC)),
|
||||
(True, ["@Alex"], datetime.date(2021, 1, 27)),
|
||||
(False, ["@Bob"], datetime.datetime(2021, 1, 27, 8, 0, tzinfo=pytz.UTC)),
|
||||
# all_day, users, start, end
|
||||
(
|
||||
False,
|
||||
["@Bernard Desruisseaux"],
|
||||
datetime.datetime(2021, 1, 26, 8, 0, tzinfo=pytz.UTC),
|
||||
datetime.datetime(2021, 1, 26, 17, 0, tzinfo=pytz.UTC),
|
||||
),
|
||||
(
|
||||
True,
|
||||
["@Alex"],
|
||||
datetime.datetime(2021, 1, 27, 0, 0, tzinfo=pytz.UTC),
|
||||
datetime.datetime(2021, 1, 27, 23, 59, 59, 999999, tzinfo=pytz.UTC),
|
||||
),
|
||||
(
|
||||
True,
|
||||
["@Alice"],
|
||||
datetime.datetime(2021, 1, 27, 0, 0, tzinfo=pytz.UTC),
|
||||
datetime.datetime(2021, 1, 28, 23, 59, 59, 999999, tzinfo=pytz.UTC),
|
||||
),
|
||||
(
|
||||
False,
|
||||
["@Bob"],
|
||||
datetime.datetime(2021, 1, 27, 8, 0, tzinfo=pytz.UTC),
|
||||
datetime.datetime(2021, 1, 27, 17, 0, tzinfo=pytz.UTC),
|
||||
),
|
||||
]
|
||||
expected = [
|
||||
{"all_day": all_day, "users": users, "start": start, "end": end}
|
||||
for all_day, users, start, end in expected_events
|
||||
]
|
||||
expected = [{"all_day": all_day, "users": users, "start": start} for all_day, users, start in expected_events]
|
||||
returned = [
|
||||
{"all_day": e["all_day"], "users": [u["display_name"] for u in e["users"]], "start": e["start"]} for e in events
|
||||
{
|
||||
"all_day": e["all_day"],
|
||||
"users": [u["display_name"] for u in e["users"]],
|
||||
"start": e["start"],
|
||||
"end": e["end"],
|
||||
}
|
||||
for e in events
|
||||
]
|
||||
assert returned == expected
|
||||
|
||||
|
|
|
|||
|
|
@ -365,7 +365,7 @@ def _get_manual_incident_form_view(routing_uid, blocks, private_metatada):
|
|||
"callback_id": routing_uid,
|
||||
"title": {
|
||||
"type": "plain_text",
|
||||
"text": "Create an Incident",
|
||||
"text": "Start New Escalation",
|
||||
},
|
||||
"close": {
|
||||
"type": "plain_text",
|
||||
|
|
|
|||
|
|
@ -501,8 +501,8 @@ class SlackEventApiEndpointView(APIView):
|
|||
return
|
||||
|
||||
text = (
|
||||
"Your Grafana account is not connected to your Slack account. :flushed:\n"
|
||||
"That's very easy to fix. Please go to the *Grafana* -> *OnCall* -> *Users*, "
|
||||
"The information in workspace is read-only. To be able to intercat with OnCall alert groups you need to connect a personal account.\n"
|
||||
"Please go to the *Grafana* -> *OnCall* -> *Users*, "
|
||||
"choose *your profile* and click the *connect* button.\n"
|
||||
":rocket: :rocket: :rocket:"
|
||||
)
|
||||
|
|
|
|||
21
engine/config_integrations/README.md
Normal file
21
engine/config_integrations/README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Contribute the new Integration to OnCall
|
||||
|
||||
Related: [DEVELOPER.md](/DEVELOPER.md)
|
||||
|
||||
"Integration" in OnCall is a pre-configured webhook for alert consumption from alert sources. Usually, alert sources are monitoring systems such as Grafana or Zabbix.
|
||||
|
||||
Integration is a set of "templates" which are dumped from the integration config once the integration is created. Further changes to "templates" don't reflect on the integration config. Read more about templates [here](https://grafana.com/docs/oncall/latest/integrations/create-custom-templates/).
|
||||
|
||||
This instruction is supposed to help you to build templates to integrate OnCall with a new source of alerts. If you don't want to contribute to OnCall and are looking for a help integrating with custom alert source as a user, refer to [this](https://grafana.com/docs/oncall/latest/integrations/create-custom-templates/) instruction.
|
||||
|
||||
# Files related to Integrations
|
||||
0. Refer to "Grafana" integration as the most complete example.
|
||||
1. Each integration should have a `{{integration_name_in_snake_case}}.py` file in `/engine/config_integrations`. There you'll find Templates that will be copied to the Integration Templates once the integration is created by the user in the OnCall UI; Example Payload; and Tests which should match the result of the rendering of Example Payload as using Templates. The best way to build such a file is to create Webhook Integration, write & debug templates in the UI first and copy-paste them to the file after.
|
||||
2. Each integration should be listed in the `/engine/settings/base.py` file, section `INSTALLED_ONCALL_INTEGRATIONS`.
|
||||
3. Each integration should have "How to connect" instruction stored as `integration_{{integration_name_in_snake_case}}.html` in the `engine/apps/integrations/html` folder. `.py` file has a `slug` field that is used to locate `.html` file.
|
||||
|
||||
# What do we expect from high-quality integration?
|
||||
|
||||
1. User-friendly integration instruction.
|
||||
2. Proper grouping following source's logics. If source generates multiple alerts per "detection" it would be nice to provide suitable grouping & resolving configuration in the templates.
|
||||
3. Awesome rendering. We all love when alerts look good in Slack, SMS and all other rendering destinations.
|
||||
62
engine/config_integrations/zabbix.py
Normal file
62
engine/config_integrations/zabbix.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# Main
|
||||
enabled = True
|
||||
title = "Zabbix"
|
||||
slug = "zabbix"
|
||||
short_description = None
|
||||
description = None
|
||||
is_displayed_on_web = True
|
||||
is_featured = False
|
||||
is_able_to_autoresolve = True
|
||||
is_demo_alert_enabled = True
|
||||
|
||||
description = None
|
||||
|
||||
# Default templates
|
||||
slack_title = """\
|
||||
*<{{ grafana_oncall_link }}|#{{ grafana_oncall_incident_id }} {{ payload.get("title", "Title undefined (check Slack Title Template)") }}>* via {{ integration_name }}
|
||||
{% if source_link %}
|
||||
(*<{{ source_link }}|source>*)
|
||||
{%- endif %}"""
|
||||
|
||||
slack_message = '{{ payload.get("message", "") }}'
|
||||
|
||||
slack_image_url = '{{ payload.get("image_url", "") }}'
|
||||
|
||||
web_title = '{{ payload.get("title", "Title undefined (check Web Title Template)") }}'
|
||||
|
||||
web_message = slack_message
|
||||
|
||||
web_image_url = slack_image_url
|
||||
|
||||
sms_title = web_title
|
||||
|
||||
phone_call_title = sms_title
|
||||
|
||||
email_title = web_title
|
||||
|
||||
email_message = web_message
|
||||
|
||||
telegram_title = sms_title
|
||||
|
||||
telegram_message = slack_message
|
||||
|
||||
telegram_image_url = slack_image_url
|
||||
|
||||
source_link = "{{ payload.link_to_upstream_details }}"
|
||||
|
||||
grouping_id = '{{ payload.get("alert_uid", "")}}'
|
||||
|
||||
resolve_condition = '{{ payload.get("state", "").upper() == "OK" }}'
|
||||
|
||||
acknowledge_condition = None
|
||||
|
||||
group_verbose_name = web_title
|
||||
|
||||
example_payload = {
|
||||
"alert_uid": "08d6891a-835c-e661-39fa-96b6a9e26552",
|
||||
"title": "TestAlert: The whole system is down",
|
||||
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
|
||||
"state": "alerting",
|
||||
"link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime",
|
||||
"message": "This alert was sent by user for the demonstration purposes\nSmth happened. Oh no!",
|
||||
}
|
||||
52
engine/engine/management/commands/verify_phone.py
Normal file
52
engine/engine/management/commands/verify_phone.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
|
||||
from apps.twilioapp.twilio_client import twilio_client
|
||||
from apps.twilioapp.utils import check_phone_number_is_valid
|
||||
from apps.user_management.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
This command is to manually verify user's phone numbers.
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("user_id", type=int, help="User id to manually verify phone number")
|
||||
parser.add_argument("phone_number", type=str, help="Phone number to verify")
|
||||
|
||||
parser.add_argument(
|
||||
"--override",
|
||||
action="store_true",
|
||||
help="Override existing phone number",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
user_id = options["user_id"]
|
||||
phone_number = options["phone_number"]
|
||||
|
||||
if not check_phone_number_is_valid(phone_number):
|
||||
self.stdout.write(self.style.ERROR('Invalid phone number "%s"' % phone_number))
|
||||
return
|
||||
|
||||
try:
|
||||
user = User.objects.get(pk=user_id)
|
||||
except User.objects.DoesNotExists:
|
||||
self.stdout.write(self.style.ERROR('Invalid user_id "%s"' % user_id))
|
||||
return
|
||||
|
||||
if user.verified_phone_number and not options["override"]:
|
||||
self.stdout.write(self.style.ERROR('User "%s" already has a phone number' % user_id))
|
||||
return
|
||||
|
||||
normalized_phone_number, _ = twilio_client.normalize_phone_number_via_twilio(phone_number)
|
||||
if normalized_phone_number:
|
||||
user.save_verified_phone_number(normalized_phone_number)
|
||||
user.unverified_phone_number = phone_number
|
||||
user.save(update_fields=["unverified_phone_number"])
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR('Invalid phone number "%s"' % phone_number))
|
||||
return
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('Successfully verified phone number "%s" for user "%s"' % (phone_number, user_id))
|
||||
)
|
||||
|
|
@ -23,19 +23,21 @@ class RequestTimeLoggingMiddleware(MiddlewareMixin):
|
|||
seconds = (dt - request._logging_start_dt).total_seconds()
|
||||
status_code = 0 if response is None else response.status_code
|
||||
content_length = request.headers.get("content-length", default=0)
|
||||
integration_type = "N/A"
|
||||
integration_token = "N/A"
|
||||
message = (
|
||||
"inbound "
|
||||
f"latency={str(seconds)} status={status_code} method={request.method} path={request.path} "
|
||||
f"content-length={content_length} slow={int(seconds > settings.SLOW_THRESHOLD_SECONDS)} "
|
||||
)
|
||||
if hasattr(request, "user") and request.user and request.user.id:
|
||||
user_id = request.user.id
|
||||
org_id = request.user.organization_id
|
||||
message += f"user_id={user_id} org_id={org_id} "
|
||||
if request.path.startswith("/integrations/v1"):
|
||||
split_path = request.path.split("/")
|
||||
integration_type = split_path[3]
|
||||
integration_token = split_path[4]
|
||||
logging.info(
|
||||
"inbound "
|
||||
f"latency={str(seconds)} status={status_code} method={request.method} path={request.path} "
|
||||
f"content-length={content_length} slow={int(seconds > settings.SLOW_THRESHOLD_SECONDS)} "
|
||||
f"integration_type={integration_type} "
|
||||
f"integration_token={integration_token}"
|
||||
)
|
||||
message += f"integration_type={integration_type} integration_token={integration_token} "
|
||||
logging.info(message)
|
||||
|
||||
def process_request(self, request):
|
||||
self.log_message(request, None, "request")
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ BASE_URL = os.environ.get("BASE_URL") # Root URL of OnCall backend
|
|||
# Feature toggles
|
||||
FEATURE_LIVE_SETTINGS_ENABLED = getenv_boolean("FEATURE_LIVE_SETTINGS_ENABLED", default=True)
|
||||
FEATURE_TELEGRAM_INTEGRATION_ENABLED = getenv_boolean("FEATURE_TELEGRAM_INTEGRATION_ENABLED", default=True)
|
||||
FEATURE_EMAIL_INTEGRATION_ENABLED = getenv_boolean("FEATURE_EMAIL_INTEGRATION_ENABLED", default=False)
|
||||
FEATURE_EMAIL_INTEGRATION_ENABLED = getenv_boolean("FEATURE_EMAIL_INTEGRATION_ENABLED", default=True)
|
||||
FEATURE_SLACK_INTEGRATION_ENABLED = getenv_boolean("FEATURE_SLACK_INTEGRATION_ENABLED", default=True)
|
||||
FEATURE_WEB_SCHEDULES_ENABLED = getenv_boolean("FEATURE_WEB_SCHEDULES_ENABLED", default=False)
|
||||
GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True)
|
||||
|
|
@ -570,7 +570,7 @@ EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER")
|
|||
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_PORT = getenv_integer("EMAIL_PORT", 587)
|
||||
EMAIL_USE_TLS = getenv_boolean("EMAIL_USE_TLS", True)
|
||||
DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL")
|
||||
EMAIL_FROM_ADDRESS = os.getenv("EMAIL_FROM_ADDRESS")
|
||||
|
||||
if FEATURE_EMAIL_INTEGRATION_ENABLED:
|
||||
EXTRA_MESSAGING_BACKENDS = [("apps.email.backend.EmailBackend", 8)]
|
||||
|
|
@ -588,6 +588,7 @@ INSTALLED_ONCALL_INTEGRATIONS = [
|
|||
"config_integrations.maintenance",
|
||||
"config_integrations.manual",
|
||||
"config_integrations.slack_channel",
|
||||
"config_integrations.zabbix",
|
||||
]
|
||||
|
||||
if OSS_INSTALLATION:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
"plugins": ["stylelint-prettier"],
|
||||
"rules": {
|
||||
"block-no-empty": [true, { "severity": "warning" }],
|
||||
"selector-max-type": [ 0, {
|
||||
"severity": "error",
|
||||
"ignore": ["child", "compounded", "descendant", "next-sibling"]
|
||||
}],
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
"scripts": {
|
||||
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 ./src",
|
||||
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --max-warnings=0 --quiet ./src",
|
||||
"stylelint": "stylelint ./src/**/*.css",
|
||||
"stylelint:fix": "stylelint --fix ./src/**/*.css",
|
||||
"stylelint": "stylelint ./src/**/*.{css,scss,module.css,module.scss}",
|
||||
"stylelint:fix": "stylelint --fix ./src/**/*.{css,scss,module.css,module.scss}",
|
||||
"build": "grafana-toolkit plugin:build",
|
||||
"test": "jest --verbose",
|
||||
"dev": "grafana-toolkit plugin:dev",
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
}
|
||||
|
||||
:global(.theme-dark) .root_bordered {
|
||||
border: var(--border);
|
||||
border: var(--border-weak);
|
||||
}
|
||||
|
||||
:global(.theme-light) .root_bordered {
|
||||
border: var(--border);
|
||||
border: var(--border-weak);
|
||||
}
|
||||
|
||||
:global(.theme-dark) .root_shadowed {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
top: -8px;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
.root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&:hover .copyButton {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.scroller {
|
||||
|
|
@ -24,6 +20,11 @@
|
|||
right: 15px;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.root:hover .copyButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,31 +6,39 @@
|
|||
&--primary {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
color: var(--warning-text-color);
|
||||
}
|
||||
|
||||
&--link {
|
||||
color: var(--primary-text-link);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&--success {
|
||||
color: var(--green-5);
|
||||
}
|
||||
|
||||
&--strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&--underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&--small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--large {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
.slack-infoblock {
|
||||
width: 725px;
|
||||
}
|
||||
|
||||
.slack-infoblock input {
|
||||
color: var(--primary-text-link);
|
||||
}
|
||||
|
||||
.slack-icon {
|
||||
width: 60px;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { Button, VerticalGroup, Icon, Field, Input } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import Text from 'components/Text/Text';
|
||||
import { SlackNewIcon } from 'icons';
|
||||
|
||||
import styles from './SlackInstructions.module.css';
|
||||
|
||||
const cx = cn.bind(styles);
|
||||
|
||||
interface SlackInstructionsProps {}
|
||||
/* This component will be used when we will work on moving ENV variables to chat-ops, but we need to do work on backend side first */
|
||||
const SlackInstructions: FC<SlackInstructionsProps> = observer(() => {
|
||||
return (
|
||||
<div>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text.Title level={2}>Connect Slack workspace</Text.Title>
|
||||
|
||||
<Block bordered withBackground className={cx('slack-infoblock')}>
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<SlackNewIcon />
|
||||
<Text>You can manage alert groups in your Slack workspace. </Text>
|
||||
<Text>Before start you need to connect your Slack bot to Grafana OnCall.</Text>
|
||||
<Text type="secondary">
|
||||
For bot creating instructions and additional information please read{' '}
|
||||
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>{' '}
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<Text>Setup environment</Text>
|
||||
<Text>
|
||||
Create OnCall Slack bot using{' '}
|
||||
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
|
||||
<Text type="link">our instructions</Text>
|
||||
</a>{' '}
|
||||
and fill out app credentials below.
|
||||
</Text>
|
||||
<div className={cx('slack-infoblock')}>
|
||||
<Field label="App ID">
|
||||
<Input id="appId" onChange={() => {}} defaultValue={'appId'} />
|
||||
</Field>
|
||||
<Field label="Client secret">
|
||||
<Input id="clientsecret" onChange={() => {}} defaultValue={'clientsecret'} />
|
||||
</Field>
|
||||
<Field label="Signing secret">
|
||||
<Input id="signingsecret" onChange={() => {}} defaultValue={'signingsecret'} />
|
||||
</Field>
|
||||
<Field label="Redirect host">
|
||||
<Input id="host" onChange={() => {}} defaultValue={'https://'} />
|
||||
</Field>
|
||||
</div>
|
||||
<Block bordered withBackground className={cx('slack-infoblock')}>
|
||||
<Text type="secondary">
|
||||
<Icon name="info-circle" /> Your host to Slack must start with “https://” and be publicly available (meaning
|
||||
that it can be reached by Slack servers). If your host is private or local, you can use redirecting services
|
||||
like Ngrok.
|
||||
</Text>
|
||||
</Block>
|
||||
<Button onClick={() => {}}>Save environment</Button>
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default SlackInstructions;
|
||||
|
|
@ -63,7 +63,7 @@ const SlackIntegrationButton = observer((props: { className: string; disabled?:
|
|||
disabled={disabled}
|
||||
onClick={onInstallModalCallback}
|
||||
>
|
||||
Install Slack integration
|
||||
Connect Slack
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
{showModal && <SlackModal onHide={onInstallModalHideCallback} onConfirm={onInstallClickCallback} />}
|
||||
|
|
@ -80,8 +80,8 @@ const SlackModal = (props: SlackModalProps) => {
|
|||
const { onHide, onConfirm } = props;
|
||||
|
||||
return (
|
||||
<Modal title="One more thing..." closeOnEscape isOpen onDismiss={onHide}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Modal title="Slack connection" closeOnEscape isOpen onDismiss={onHide}>
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
You can view your Slack Workspace at the top-right corner after you are redirected. It should be a Workspace
|
||||
with App Bot installed:
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,3 +19,18 @@
|
|||
.telegram-instruction-cancel {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.telegram-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-command {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.infoblock-text {
|
||||
margin-left: 48px;
|
||||
margin-right: 48px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { Button, Modal, Icon, HorizontalGroup } from '@grafana/ui';
|
||||
import { Button, Modal, Icon, HorizontalGroup, VerticalGroup, Field, Input } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import Text from 'components/Text/Text';
|
||||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
|
@ -44,7 +45,7 @@ const TelegramIntegrationButton = observer((props: TelegramIntegrationProps) =>
|
|||
<>
|
||||
<WithPermissionControl userAction={UserAction.UpdateIntegrations}>
|
||||
<Button size={size} variant="primary" icon="plus" disabled={disabled} onClick={onInstallModalCallback}>
|
||||
Connect Telegram channel
|
||||
Add Telegram channel
|
||||
</Button>
|
||||
</WithPermissionControl>
|
||||
{showModal && <TelegramModal onHide={onInstallModalHideCallback} onUpdate={onModalUpdateCallback} />}
|
||||
|
|
@ -73,101 +74,66 @@ const TelegramModal = (props: TelegramModalProps) => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<Modal title="Connect Telegram Channel" closeOnEscape isOpen onDismiss={onUpdate}>
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text.Title level={5}>Follow these steps to create and connect to a dedicated OnCall channel.</Text.Title>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>
|
||||
If you already have a dedicated channel to use with OnCall, you can use the following activation code:{' '}
|
||||
<Text className={cx('verification-code')}>{verificationCode}</Text>
|
||||
<span className={cx('copy-icon')}>
|
||||
<CopyToClipboard
|
||||
text={verificationCode}
|
||||
onCopy={() => {
|
||||
openNotification('Verification code copied');
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
</span>
|
||||
<Modal title="Adding Telegram Channel" closeOnEscape isOpen onDismiss={onUpdate}>
|
||||
<VerticalGroup spacing="md">
|
||||
<Block withBackground bordered className={cx('telegram-block')}>
|
||||
<Text type="secondary">
|
||||
If you already have a private channel to work with OnCall, use the following activation code:
|
||||
</Text>
|
||||
<Field className={cx('field-command')}>
|
||||
<Input
|
||||
id="telegramVerificationCode"
|
||||
value={verificationCode}
|
||||
suffix={
|
||||
<CopyToClipboard
|
||||
text={verificationCode}
|
||||
onCopy={() => {
|
||||
openNotification('Code is copied');
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</Block>
|
||||
<Text.Title level={5}>Setup new channel</Text.Title>
|
||||
<Text type="secondary">
|
||||
1. Open Telegram, create a new <Text type="primary">Private Channel</Text> and enable{' '}
|
||||
<Text type="primary">Sign Messages</Text> in settings.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>1. Create a New Channel, and set it to Private.</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>
|
||||
2. In <b>Manage Channel</b>, make sure <b>Sign messages</b> is enabled.
|
||||
<Text type="secondary">
|
||||
2. Create a new <Text type="primary">Discussion group</Text>. This group handles alert actions and comments.{' '}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>3. Create a new discussion group. This group handles alert actions and comments.</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>
|
||||
4. Add the discussion group to the channel. In <b>Manage Channel</b>, click <b>Discussion</b> to find and add
|
||||
the new group.
|
||||
<Text type="secondary">
|
||||
3. Connect the discussion group with the channel. In <Text type="primary">Manage Channel</Text>, click{' '}
|
||||
<Text type="primary">Discussion</Text> to find and add your group.{' '}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>
|
||||
5. Click{' '}
|
||||
<a className={cx('telegram-bot')} href={botLink} target="_blank" rel="noreferrer">
|
||||
{botLink}
|
||||
</a>{' '}
|
||||
to add the OnCall bot to your contacts. Add the bot to your channel as an Admin. Allow it to{' '}
|
||||
<b>Post Messages</b>.
|
||||
<Text type="secondary">
|
||||
4. Go to <Text type="link">{botLink}</Text> to add the OnCall bot to your contacts. Then add the bot to your
|
||||
channel as an <Text type="primary">Admin</Text> and allow it to <Text type="primary">Post Messages</Text>.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>6. Add the bot to the discussion group.</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>
|
||||
7. Send the verification code, <Text className={cx('verification-code')}>{verificationCode}</Text>
|
||||
<span className={cx('copy-icon')}>
|
||||
<CopyToClipboard
|
||||
text={verificationCode}
|
||||
onCopy={() => {
|
||||
openNotification('Verification code copied');
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
</span>{' '}
|
||||
, to the channel and wait for the confirmation message.
|
||||
<Text type="secondary">5. Add the bot to the discussion group.</Text>
|
||||
<Text type="secondary">
|
||||
6. Send this verification code to the channel and wait for the confirmation message:
|
||||
<Field className={cx('field-command')}>
|
||||
<Input
|
||||
id="telegramVerificationCode"
|
||||
value={verificationCode}
|
||||
suffix={
|
||||
<CopyToClipboard
|
||||
text={verificationCode}
|
||||
onCopy={() => {
|
||||
openNotification('Code is copied');
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>8. Make sure users connect their Telegram accounts in their OnCall user profile.</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>9. Done! Now you can manage alerts in your Telegram workspace.</Text>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-container')}>
|
||||
<Text>
|
||||
Each alert group notification is assigned a dedicated discussion. Users can perform notification actions
|
||||
(acknowledge, resolve, silence) and discuss alerts in the comments section of the discussions.
|
||||
</Text>
|
||||
<img
|
||||
style={{ height: '350px', display: 'block', margin: '20px auto' }}
|
||||
src="public/plugins/grafana-oncall-app/img/telegram_discussion.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={cx('telegram-instruction-cancel')}>
|
||||
<Text type="secondary">7. Start to manage alerts in your team Telegram workspace.</Text>
|
||||
<HorizontalGroup justify="flex-end">
|
||||
<Button variant="secondary" onClick={onHide}>
|
||||
Cancel
|
||||
|
|
@ -176,7 +142,7 @@ const TelegramModal = (props: TelegramModalProps) => {
|
|||
Done
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</VerticalGroup>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,3 +2,13 @@
|
|||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.slack-infoblock {
|
||||
text-align: center;
|
||||
width: 725px;
|
||||
}
|
||||
|
||||
.external-link-style {
|
||||
margin-right: 4px;
|
||||
align-self: baseline;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, VerticalGroup } from '@grafana/ui';
|
||||
import { Button, VerticalGroup, Icon } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import Text from 'components/Text/Text';
|
||||
import { SlackNewIcon } from 'icons';
|
||||
import { useStore } from 'state/useStore';
|
||||
|
||||
import styles from './SlackTab.module.css';
|
||||
|
|
@ -18,20 +20,32 @@ export const SlackTab = () => {
|
|||
}, [slackStore]);
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<Text>
|
||||
You can view your Slack Workspace at the top-right corner after you are redirected. It should be a Workspace
|
||||
with App Bot installed:
|
||||
</Text>
|
||||
<img
|
||||
style={{ height: '350px', display: 'block', margin: '0 auto' }}
|
||||
src="public/plugins/grafana-oncall-app/img/slack_workspace_choose_attention.png"
|
||||
/>
|
||||
<div className={cx('footer')}>
|
||||
<Button key="back" onClick={handleClickConnectSlackAccount}>
|
||||
I'll check! Proceed to Slack...
|
||||
</Button>
|
||||
</div>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Block bordered withBackground className={cx('slack-infoblock', 'personal-slack-infoblock')}>
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<SlackNewIcon />
|
||||
<Text>
|
||||
Personal Slack connection will allow you to manage alert grouops in your connected team Internal Slack
|
||||
workspace.
|
||||
</Text>
|
||||
<Text>To setup personal Slack click the button below, choose workspace and click Allow.</Text>
|
||||
|
||||
<Text type="secondary">
|
||||
More details in{' '}
|
||||
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>
|
||||
|
||||
<img
|
||||
style={{ height: '350px', display: 'block', margin: '0 auto' }}
|
||||
src="public/plugins/grafana-oncall-app/img/slack_instructions.png"
|
||||
/>
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<Button onClick={handleClickConnectSlackAccount}>
|
||||
<Icon name="external-link-alt" className={cx('external-link-style')} /> Open Slack connection page
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,3 +6,15 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.automatic-connect-telegram-block {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.field-command {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import React, { HTMLAttributes, useEffect, useState } from 'react';
|
||||
|
||||
import { Alert, Button, HorizontalGroup, Icon, VerticalGroup } from '@grafana/ui';
|
||||
import { Button, Icon, VerticalGroup, Field, Input } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import { TelegramColorIcon } from 'icons';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { useStore } from 'state/useStore';
|
||||
import { openNotification } from 'utils';
|
||||
|
|
@ -37,45 +39,66 @@ const TelegramInfo = observer((_props: TelegramInfoProps) => {
|
|||
<>
|
||||
{telegramConfigured || !store.hasFeature(AppFeature.LiveSettings) ? (
|
||||
<VerticalGroup>
|
||||
<a href={`${botLink}/?start=${verificationCode}`} target="_blank" rel="noreferrer">
|
||||
<Button size="sm" fill="outline">
|
||||
Connect automatically
|
||||
</Button>
|
||||
</a>
|
||||
<Text>Or add bot manually:</Text>
|
||||
<HorizontalGroup>
|
||||
<Text>
|
||||
1) Go to{' '}
|
||||
<a className={cx('verification-code')} href={botLink} target="_blank" rel="noreferrer">
|
||||
{botLink}
|
||||
</a>
|
||||
</Text>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup className={cx('verification-code-text')}>
|
||||
<Text>2) Send </Text>
|
||||
<Text className={cx('verification-code')}>{verificationCode}</Text>
|
||||
<CopyToClipboard
|
||||
text={verificationCode}
|
||||
onCopy={() => {
|
||||
openNotification('Verification code copied');
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
<Text>to telegram bot </Text>
|
||||
</HorizontalGroup>
|
||||
<Text.Title level={5}>Connect personal Telegram</Text.Title>
|
||||
<Block bordered withBackground className={cx('automatic-connect-telegram-block')}>
|
||||
<Text type="secondary">Connect Telegram automatically</Text>
|
||||
<a href={`${botLink}/?start=${verificationCode}`} target="_blank" rel="noreferrer">
|
||||
<Button size="sm">Connect account</Button>
|
||||
</a>
|
||||
</Block>
|
||||
<Text.Title level={5}>Manual connection</Text.Title>
|
||||
|
||||
<Text type="secondary">
|
||||
1. Go to{' '}
|
||||
<a className={cx('verification-code')} href={botLink} target="_blank" rel="noreferrer">
|
||||
{botLink}
|
||||
</a>
|
||||
</Text>
|
||||
|
||||
<Text type="secondary">
|
||||
2. Send this verification code to the bot and wait for <Text>the confirmation message: </Text>
|
||||
</Text>
|
||||
<Field className={cx('field-command')}>
|
||||
<Input
|
||||
id="telegramVerificationCode"
|
||||
value={verificationCode}
|
||||
suffix={
|
||||
<CopyToClipboard
|
||||
text={verificationCode}
|
||||
onCopy={() => {
|
||||
openNotification('Code is copied');
|
||||
}}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</CopyToClipboard>
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Text type="secondary">3. Refresh the page and start to manage alerts in your personal Telegram.</Text>
|
||||
</VerticalGroup>
|
||||
) : (
|
||||
<Alert
|
||||
severity="warning"
|
||||
// @ts-ignore
|
||||
title={
|
||||
<>
|
||||
Can't connect Telegram. <PluginLink query={{ page: 'live-settings' }}> Check ENV variables</PluginLink>{' '}
|
||||
related to Telegram.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text.Title level={2}>Connect Telegram workspace</Text.Title>
|
||||
<Block bordered withBackground className={cx('telegram-infoblock')}>
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<TelegramColorIcon />
|
||||
<Text>You can manage alert groups in your team Telegram channel or from personal direct messages. </Text>
|
||||
|
||||
<Text>
|
||||
To connect channel setup Telegram environment first, which includes connection to your bot and host URL.
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
More details in{' '}
|
||||
<a href="https://grafana.com/docs/grafana-cloud/oncall/chat-options/configure-telegram/">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<PluginLink query={{ page: 'live-settings' }}>
|
||||
<Button variant="primary">Setup ENV Variables</Button>
|
||||
</PluginLink>
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -273,3 +273,59 @@ export const IsOncallIcon = (props: IsOncallIconProps) => {
|
|||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TelegramColorIcon = () => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
|
||||
<path fill="#29b6f6" d="M24 4A20 20 0 1 0 24 44A20 20 0 1 0 24 4Z" />
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M33.95,15l-3.746,19.126c0,0-0.161,0.874-1.245,0.874c-0.576,0-0.873-0.274-0.873-0.274l-8.114-6.733 l-3.97-2.001l-5.095-1.355c0,0-0.907-0.262-0.907-1.012c0-0.625,0.933-0.923,0.933-0.923l21.316-8.468 c-0.001-0.001,0.651-0.235,1.126-0.234C33.667,14,34,14.125,34,14.5C34,14.75,33.95,15,33.95,15z"
|
||||
/>
|
||||
<path
|
||||
fill="#b0bec5"
|
||||
d="M23,30.505l-3.426,3.374c0,0-0.149,0.115-0.348,0.12c-0.069,0.002-0.143-0.009-0.219-0.043 l0.964-5.965L23,30.505z"
|
||||
/>
|
||||
<path
|
||||
fill="#cfd8dc"
|
||||
d="M29.897,18.196c-0.169-0.22-0.481-0.26-0.701-0.093L16,26c0,0,2.106,5.892,2.427,6.912 c0.322,1.021,0.58,1.045,0.58,1.045l0.964-5.965l9.832-9.096C30.023,18.729,30.064,18.416,29.897,18.196z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const SlackNewIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px">
|
||||
<path
|
||||
fill="#33d375"
|
||||
d="M33,8c0-2.209-1.791-4-4-4s-4,1.791-4,4c0,1.254,0,9.741,0,11c0,2.209,1.791,4,4,4s4-1.791,4-4 C33,17.741,33,9.254,33,8z"
|
||||
/>
|
||||
<path
|
||||
fill="#33d375"
|
||||
d="M43,19c0,2.209-1.791,4-4,4c-1.195,0-4,0-4,0s0-2.986,0-4c0-2.209,1.791-4,4-4S43,16.791,43,19z"
|
||||
/>
|
||||
<path
|
||||
fill="#40c4ff"
|
||||
d="M8,14c-2.209,0-4,1.791-4,4s1.791,4,4,4c1.254,0,9.741,0,11,0c2.209,0,4-1.791,4-4s-1.791-4-4-4 C17.741,14,9.254,14,8,14z"
|
||||
/>
|
||||
<path
|
||||
fill="#40c4ff"
|
||||
d="M19,4c2.209,0,4,1.791,4,4c0,1.195,0,4,0,4s-2.986,0-4,0c-2.209,0-4-1.791-4-4S16.791,4,19,4z"
|
||||
/>
|
||||
<path
|
||||
fill="#e91e63"
|
||||
d="M14,39.006C14,41.212,15.791,43,18,43s4-1.788,4-3.994c0-1.252,0-9.727,0-10.984 c0-2.206-1.791-3.994-4-3.994s-4,1.788-4,3.994C14,29.279,14,37.754,14,39.006z"
|
||||
/>
|
||||
<path
|
||||
fill="#e91e63"
|
||||
d="M4,28.022c0-2.206,1.791-3.994,4-3.994c1.195,0,4,0,4,0s0,2.981,0,3.994c0,2.206-1.791,3.994-4,3.994 S4,30.228,4,28.022z"
|
||||
/>
|
||||
<path
|
||||
fill="#ffc107"
|
||||
d="M39,33c2.209,0,4-1.791,4-4s-1.791-4-4-4c-1.254,0-9.741,0-11,0c-2.209,0-4,1.791-4,4s1.791,4,4,4 C29.258,33,37.746,33,39,33z"
|
||||
/>
|
||||
<path
|
||||
fill="#ffc107"
|
||||
d="M28,43c-2.209,0-4-1.791-4-4c0-1.195,0-4,0-4s2.986,0,4,0c2.209,0,4,1.791,4,4S30.209,43,28,43z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
|
|
|||
BIN
grafana-plugin/src/img/slack_instructions.png
Normal file
BIN
grafana-plugin/src/img/slack_instructions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
|
|
@ -15,3 +15,27 @@
|
|||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(204, 204, 220, 0.25);
|
||||
}
|
||||
|
||||
.slack-infoblock {
|
||||
text-align: center;
|
||||
width: 725px;
|
||||
}
|
||||
|
||||
.external-link-style {
|
||||
margin-right: 4px;
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.team_workspace {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.infoblock-text {
|
||||
margin-left: 48px;
|
||||
margin-right: 48px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.infoblock-icon {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Field, HorizontalGroup, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { Field, HorizontalGroup, LoadingPlaceholder, VerticalGroup, Icon, Button } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import Tutorial from 'components/Tutorial/Tutorial';
|
||||
import { TutorialStep } from 'components/Tutorial/Tutorial.types';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import GSelect from 'containers/GSelect/GSelect';
|
||||
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
|
||||
import SlackIntegrationButton from 'containers/SlackIntegrationButton/SlackIntegrationButton';
|
||||
import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl';
|
||||
import { SlackNewIcon } from 'icons';
|
||||
import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config';
|
||||
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
|
|
@ -25,16 +25,32 @@ const cx = cn.bind(styles);
|
|||
|
||||
interface SlackProps extends WithStoreProps {}
|
||||
|
||||
interface SlackState {}
|
||||
interface SlackState {
|
||||
showENVVariablesButton: boolean;
|
||||
}
|
||||
|
||||
@observer
|
||||
class SlackSettings extends Component<SlackProps, SlackState> {
|
||||
state: SlackState = {};
|
||||
state: SlackState = {
|
||||
showENVVariablesButton: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.update();
|
||||
const { store } = this.props;
|
||||
if (store.hasFeature(AppFeature.LiveSettings)) {
|
||||
this.getSlackLiveSettings().then(() => {
|
||||
this.update();
|
||||
});
|
||||
} else {
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
handleOpenSlackInstructions = () => {
|
||||
const { store } = this.props;
|
||||
store.slackStore.installSlackIntegration();
|
||||
};
|
||||
|
||||
update = () => {
|
||||
const { store } = this.props;
|
||||
|
||||
|
|
@ -42,6 +58,27 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
store.slackStore.updateSlackSettings();
|
||||
};
|
||||
|
||||
getSlackLiveSettings = async () => {
|
||||
const { store } = this.props;
|
||||
const results = await store.globalSettingStore.getAll();
|
||||
|
||||
const slackClientOAUTH = results.find((element: { name: string }) => element.name === 'SLACK_CLIENT_OAUTH_ID');
|
||||
const slackClientOAUTHSecret = results.find(
|
||||
(element: { name: string }) => element.name === 'SLACK_CLIENT_OAUTH_SECRET'
|
||||
);
|
||||
const slackRedirectHost = results.find((element: { name: string }) => element.name === 'SLACK_CLIENT_OAUTH_ID');
|
||||
const slackSigningSecret = results.find((element: { name: string }) => element.name === 'SLACK_SIGNING_SECRET');
|
||||
|
||||
if (
|
||||
slackClientOAUTH?.error ||
|
||||
slackClientOAUTHSecret?.error ||
|
||||
slackRedirectHost?.error ||
|
||||
slackSigningSecret?.error
|
||||
) {
|
||||
this.setState({ showENVVariablesButton: true });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { store } = this.props;
|
||||
const { teamStore } = store;
|
||||
|
|
@ -59,34 +96,47 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
|
||||
return (
|
||||
<div className={cx('root')}>
|
||||
<Text.Title level={4} className={cx('title')}>
|
||||
Slack
|
||||
</Text.Title>
|
||||
<div className={cx('slack-settings')}>
|
||||
<Field label="Default channel for Slack notifications">
|
||||
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
|
||||
<GSelect
|
||||
showSearch
|
||||
className={cx('select', 'control')}
|
||||
modelName="slackChannelStore"
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
value={teamStore.currentTeam?.slack_channel?.id}
|
||||
onChange={this.handleSlackChannelChange}
|
||||
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||
/>
|
||||
</WithPermissionControl>
|
||||
</Field>
|
||||
<div className={cx('title')}>
|
||||
<Text.Title level={3}>Slack</Text.Title>
|
||||
</div>
|
||||
<div className={cx('slack-settings')}>
|
||||
<Text.Title level={4} className={cx('title')}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<HorizontalGroup align="center">
|
||||
<Field label="Slack Workspace">
|
||||
<div className={cx('select', 'control', 'team_workspace')}>
|
||||
<Text>{store.teamStore.currentTeam.slack_team_identity?.cached_name}</Text>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Default channel for Slack notifications">
|
||||
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
|
||||
<GSelect
|
||||
showSearch
|
||||
className={cx('select', 'control')}
|
||||
modelName="slackChannelStore"
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
value={teamStore.currentTeam?.slack_channel?.id}
|
||||
onChange={this.handleSlackChannelChange}
|
||||
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||
/>
|
||||
</WithPermissionControl>
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
<WithPermissionControl userAction={UserAction.UpdateIntegrations}>
|
||||
<WithConfirm title="Are you sure to delete this Slack Integration?">
|
||||
<Button variant="destructive" size="sm" onClick={() => this.removeSlackIntegration()}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</WithConfirm>
|
||||
</WithPermissionControl>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={cx('slack-settings')}>
|
||||
<Text.Title level={5} className={cx('title')}>
|
||||
Additional settings
|
||||
</Text.Title>
|
||||
<Field
|
||||
label="Timeout for acknowledged alerts"
|
||||
description="Set up a reminder and timeout for acknowledged alert to never forget about them"
|
||||
>
|
||||
<Field label="Timeout for acknowledged alerts">
|
||||
<HorizontalGroup>
|
||||
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
|
||||
<RemoteSelect
|
||||
|
|
@ -110,14 +160,51 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
</HorizontalGroup>
|
||||
</Field>
|
||||
</div>
|
||||
<Text.Title level={4} className={cx('title')}>
|
||||
Remove integration
|
||||
</Text.Title>
|
||||
<SlackIntegrationButton className={cx('slack-button')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderSlackWorkspace = () => {
|
||||
const { store } = this.props;
|
||||
return <Text>{store.teamStore.currentTeam.slack_team_identity?.cached_name}</Text>;
|
||||
};
|
||||
|
||||
renderSlackChannels = () => {
|
||||
const { store } = this.props;
|
||||
return (
|
||||
<WithPermissionControl userAction={UserAction.UpdateGeneralLogChannelId}>
|
||||
<GSelect
|
||||
showSearch
|
||||
className={cx('select', 'control')}
|
||||
modelName="slackChannelStore"
|
||||
displayField="display_name"
|
||||
valueField="id"
|
||||
placeholder="Select Slack Channel"
|
||||
value={store.teamStore.currentTeam?.slack_channel?.id}
|
||||
onChange={this.handleSlackChannelChange}
|
||||
nullItemName={PRIVATE_CHANNEL_NAME}
|
||||
/>
|
||||
</WithPermissionControl>
|
||||
);
|
||||
};
|
||||
|
||||
renderActionButtons = () => {
|
||||
<WithPermissionControl userAction={UserAction.UpdateIntegrations}>
|
||||
<WithConfirm title="Are you sure to delete this Slack Integration?">
|
||||
<Button variant="destructive" size="sm" onClick={() => this.removeSlackIntegration()}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</WithConfirm>
|
||||
</WithPermissionControl>;
|
||||
};
|
||||
|
||||
removeSlackIntegration = () => {
|
||||
const { store } = this.props;
|
||||
store.slackStore.removeSlackIntegration().then(() => {
|
||||
store.teamStore.loadCurrentTeam();
|
||||
});
|
||||
};
|
||||
|
||||
getSlackSettingsChangeHandler = (field: string) => {
|
||||
const { store } = this.props;
|
||||
const { slackStore } = store;
|
||||
|
|
@ -138,28 +225,56 @@ class SlackSettings extends Component<SlackProps, SlackState> {
|
|||
|
||||
renderSlackStub = () => {
|
||||
const { store } = this.props;
|
||||
const { showENVVariablesButton } = this.state;
|
||||
const isLiveSettingAvailable = store.hasFeature(AppFeature.LiveSettings) && showENVVariablesButton;
|
||||
|
||||
return (
|
||||
<Tutorial
|
||||
step={TutorialStep.Slack}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">
|
||||
Bring the whole incident lifecycle to Slack, from alerts, monitoring, escalations to resolution notes and
|
||||
reports.
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text.Title level={2}>Connect Slack workspace</Text.Title>
|
||||
<Block bordered withBackground className={cx('slack-infoblock')}>
|
||||
<VerticalGroup align="center">
|
||||
<div className={cx('infoblock-icon')}>
|
||||
<SlackNewIcon />
|
||||
</div>
|
||||
<Text className={cx('infoblock-text')}>
|
||||
Slack connection will allow you to manage alert groups in your team Slack workspace.
|
||||
</Text>
|
||||
|
||||
<SlackIntegrationButton className={cx('slack-button')} />
|
||||
|
||||
{store.hasFeature(AppFeature.LiveSettings) && (
|
||||
<Text type="secondary">
|
||||
Before installing <PluginLink query={{ page: 'live-settings' }}>check ENV variables</PluginLink> related
|
||||
to Slack please
|
||||
<Text className={cx('infoblock-text')}>
|
||||
After a basic workspace connection your team members need to connect their personal Slack accounts in
|
||||
order to be allowed to manage alert groups.
|
||||
</Text>
|
||||
{isLiveSettingAvailable && (
|
||||
<Text type="secondary" className={cx('infoblock-text')}>
|
||||
For bot creating instructions and additional information please read{' '}
|
||||
<a href="https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<img
|
||||
style={{ height: '350px', display: 'block', margin: '0 auto' }}
|
||||
src="public/plugins/grafana-oncall-app/img/slack_instructions.png"
|
||||
/>
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
</Block>
|
||||
{isLiveSettingAvailable ? (
|
||||
<PluginLink query={{ page: 'live-settings' }}>
|
||||
<Button variant="primary">Setup ENV Variables</Button>
|
||||
</PluginLink>
|
||||
) : (
|
||||
<HorizontalGroup>
|
||||
<Button onClick={this.handleOpenSlackInstructions}>
|
||||
<Icon name="external-link-alt" className={cx('external-link-style')} /> Open Slack connection page
|
||||
</Button>
|
||||
{store.hasFeature(AppFeature.LiveSettings) && (
|
||||
<PluginLink query={{ page: 'live-settings' }}>
|
||||
<Button variant="secondary">See ENV Variables</Button>
|
||||
</PluginLink>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
)}
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,3 +6,22 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.telegram-infoblock {
|
||||
text-align: center;
|
||||
width: 725px;
|
||||
}
|
||||
|
||||
.features-list > ul {
|
||||
margin: 20px 30px;
|
||||
}
|
||||
|
||||
.infoblock-text {
|
||||
margin-left: 48px;
|
||||
margin-right: 48px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.infoblock-icon {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Alert, Button, HorizontalGroup, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import { Badge, Button, HorizontalGroup, Icon, LoadingPlaceholder, VerticalGroup } from '@grafana/ui';
|
||||
import cn from 'classnames/bind';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
import Block from 'components/GBlock/Block';
|
||||
import GTable from 'components/GTable/GTable';
|
||||
import PluginLink from 'components/PluginLink/PluginLink';
|
||||
import Text from 'components/Text/Text';
|
||||
import Tutorial from 'components/Tutorial/Tutorial';
|
||||
import { TutorialStep } from 'components/Tutorial/Tutorial.types';
|
||||
import WithConfirm from 'components/WithConfirm/WithConfirm';
|
||||
import TelegramIntegrationButton from 'containers/TelegramIntegrationButton/TelegramIntegrationButton';
|
||||
import { TelegramColorIcon } from 'icons';
|
||||
import { TelegramChannel } from 'models/telegram_channel/telegram_channel.types';
|
||||
import { AppFeature } from 'state/features';
|
||||
import { WithStoreProps } from 'state/types';
|
||||
|
|
@ -47,16 +47,32 @@ class TelegramSettings extends Component<TelegramProps, TelegramState> {
|
|||
|
||||
if (!telegramConfigured && store.hasFeature(AppFeature.LiveSettings)) {
|
||||
return (
|
||||
<Alert
|
||||
severity="warning"
|
||||
// @ts-ignore
|
||||
title={
|
||||
<>
|
||||
Can't connect Telegram. <PluginLink query={{ page: 'live-settings' }}> Check ENV variables</PluginLink>{' '}
|
||||
related to Telegram.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text.Title level={2}>Connect Telegram workspace</Text.Title>
|
||||
<Block bordered withBackground className={cx('telegram-infoblock')}>
|
||||
<VerticalGroup align="center">
|
||||
<div className={cx('infoblock-icon')}>
|
||||
<TelegramColorIcon />
|
||||
</div>
|
||||
<Text className={cx('infoblock-text')}>
|
||||
You can manage alert groups in your team Telegram channel or from personal direct messages.{' '}
|
||||
</Text>
|
||||
|
||||
<Text className={cx('infoblock-text')}>
|
||||
To connect channel setup Telegram environment first, which includes connection to your bot and host URL.
|
||||
</Text>
|
||||
<Text type="secondary" className={cx('infoblock-text')}>
|
||||
More details in{' '}
|
||||
<a href="https://grafana.com/docs/grafana-cloud/oncall/chat-options/configure-telegram/">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>
|
||||
</VerticalGroup>
|
||||
</Block>
|
||||
<PluginLink query={{ page: 'live-settings' }}>
|
||||
<Button variant="primary">Setup ENV Variables</Button>
|
||||
</PluginLink>
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -66,38 +82,59 @@ class TelegramSettings extends Component<TelegramProps, TelegramState> {
|
|||
|
||||
if (!connectedChannels.length) {
|
||||
return (
|
||||
<Tutorial
|
||||
step={TutorialStep.Slack}
|
||||
title={
|
||||
<VerticalGroup align="center" spacing="lg">
|
||||
<Text type="secondary">
|
||||
Bring the whole incident lifecycle into your chat workspace. Everything from alerts, monitoring, and
|
||||
escalations to reports.
|
||||
<VerticalGroup spacing="lg">
|
||||
<Text.Title level={2}>Connect Telegram workspace</Text.Title>
|
||||
<Block bordered withBackground className={cx('telegram-infoblock')}>
|
||||
<VerticalGroup align="center">
|
||||
<div className={cx('infoblock-icon')}>
|
||||
<TelegramColorIcon />
|
||||
</div>
|
||||
<Text className={cx('infoblock-text')}>
|
||||
You can manage alert groups in your team Telegram channel or from personal direct messages.{' '}
|
||||
</Text>
|
||||
<Text type="secondary" className={cx('infoblock-text')}>
|
||||
More details in{' '}
|
||||
<a href="https://grafana.com/docs/grafana-cloud/oncall/chat-options/configure-telegram/">
|
||||
<Text type="link">our documentation</Text>
|
||||
</a>
|
||||
</Text>
|
||||
<TelegramIntegrationButton size="lg" onUpdate={this.update} />
|
||||
</VerticalGroup>
|
||||
}
|
||||
/>
|
||||
</Block>
|
||||
<Text>
|
||||
<Text.Title level={4}>Features</Text.Title>
|
||||
<div className={cx('features-list')}>
|
||||
<ul>
|
||||
<li>perform actions (acknowledge, resolve, silence)</li>
|
||||
<li>discuss alerts in comments</li>
|
||||
<li>notifications to users accounts will be served as links to the main channel</li>
|
||||
</ul>
|
||||
</div>
|
||||
Make sure your team connects Telegram in their OnCall user profiles too or they cannot manage alert groups.
|
||||
</Text>
|
||||
<HorizontalGroup>
|
||||
<TelegramIntegrationButton size="md" onUpdate={this.update} />
|
||||
{store.hasFeature(AppFeature.LiveSettings) && (
|
||||
<PluginLink query={{ page: 'live-settings' }}>
|
||||
<Button variant="secondary">See ENV Variables</Button>
|
||||
</PluginLink>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</VerticalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
width: '30%',
|
||||
title: 'Channel name',
|
||||
dataIndex: 'channel_name',
|
||||
width: '35%',
|
||||
title: 'Channel',
|
||||
key: 'name',
|
||||
render: this.renderChannelName,
|
||||
},
|
||||
{
|
||||
width: '30%',
|
||||
title: 'Discussion group name',
|
||||
width: '35%',
|
||||
title: 'Discussion group',
|
||||
dataIndex: 'discussion_group_name',
|
||||
},
|
||||
{
|
||||
width: '10%',
|
||||
title: 'Is default channel',
|
||||
dataIndex: 'is_default_channel',
|
||||
render: this.renderDefaultChannel,
|
||||
},
|
||||
{
|
||||
width: '30%',
|
||||
key: 'action',
|
||||
|
|
@ -126,6 +163,13 @@ class TelegramSettings extends Component<TelegramProps, TelegramState> {
|
|||
);
|
||||
}
|
||||
|
||||
renderChannelName = (record: TelegramChannel) => {
|
||||
return (
|
||||
<>
|
||||
{record.channel_name} {record.is_default_channel && <Badge text="Default" color="green" />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
renderDefaultChannel = (isDefault: boolean) => {
|
||||
return <>{isDefault && <Icon name="check" />}</>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -89,13 +89,13 @@ class LiveSettings extends React.Component<LiveSettingsProps, LiveSettingsState>
|
|||
{
|
||||
width: '20%',
|
||||
title: 'Value',
|
||||
render: this.renderValue,
|
||||
render: (item: GlobalSetting) => this.renderValue(item), // to avoid caching previous render result
|
||||
key: 'value',
|
||||
},
|
||||
{
|
||||
width: '20%',
|
||||
title: 'ENV or default',
|
||||
render: this.renderDefault,
|
||||
render: (item: GlobalSetting) => this.renderDefault(item), // to avoid caching previous render result
|
||||
key: 'default',
|
||||
},
|
||||
{
|
||||
|
|
|
|||
24
helm/oncall/Chart.lock
Normal file
24
helm/oncall/Chart.lock
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
dependencies:
|
||||
- name: cert-manager
|
||||
repository: https://charts.jetstack.io
|
||||
version: v1.8.0
|
||||
- name: mariadb
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 11.0.10
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 11.9.10
|
||||
- name: rabbitmq
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 10.1.1
|
||||
- name: redis
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 16.10.1
|
||||
- name: grafana
|
||||
repository: https://grafana.github.io/helm-charts
|
||||
version: 6.29.6
|
||||
- name: ingress-nginx
|
||||
repository: https://kubernetes.github.io/ingress-nginx
|
||||
version: 4.1.4
|
||||
digest: sha256:8e17f2f6a087b6db52670458fc0e1cb39b0a3f7962ff7ebbc7be4c982a4e1720
|
||||
generated: "2022-10-18T11:22:39.061819+02:00"
|
||||
|
|
@ -8,13 +8,13 @@ type: application
|
|||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.0.6
|
||||
version: 1.0.8
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v1.0.40"
|
||||
appVersion: "v1.0.49"
|
||||
dependencies:
|
||||
- name: cert-manager
|
||||
version: v1.8.0
|
||||
|
|
@ -25,6 +25,10 @@ dependencies:
|
|||
version: 11.0.10
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: mariadb.enabled
|
||||
- name: postgresql
|
||||
version: 11.9.10
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: postgresql.enabled
|
||||
- name: rabbitmq
|
||||
version: 10.1.1
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
|
|
|
|||
|
|
@ -6,23 +6,27 @@ It will also deploy cert manager and nginx ingress controller, as Grafana OnCall
|
|||
to receive alerts from other monitoring systems. Grafana OnCall engine acts as a backend and can be connected to the Grafana frontend plugin named Grafana OnCall.
|
||||
Architecture diagram can be found [here](https://raw.githubusercontent.com/grafana/oncall/dev/docs/img/architecture_diagram.png)
|
||||
|
||||
## Production usage
|
||||
|
||||
### Production usage
|
||||
**Default helm chart configuration is not intended for production.** The helm chart includes all the services into a single release, which is not recommended for production usage. It is recommended to run stateful services such as MySQL and RabbitMQ separately from this release or use managed PaaS solutions. It will significantly reduce the overhead of managing them. Here are the instructions on how to set up your own [ingress](#set-up-external-access), [MySQL](#connect-external-mysql), [RabbitMQ](#connect-external-rabbitmq), [Redis](#connect-external-redis)
|
||||
|
||||
|
||||
### Cluster requirements
|
||||
|
||||
* ensure you can run x86-64/amd64 workloads. arm64 architecture is currently not supported
|
||||
* kubernetes version 1.25+ is not supported, if cert-manager is enabled
|
||||
|
||||
## Install
|
||||
|
||||
### Prepare the repo
|
||||
```
|
||||
|
||||
```bash
|
||||
# Add the repository
|
||||
helm repo add grafana https://grafana.github.io/helm-charts
|
||||
helm repo update
|
||||
```
|
||||
|
||||
### Installing the helm chart
|
||||
|
||||
```bash
|
||||
# Install the chart
|
||||
helm install \
|
||||
|
|
@ -34,7 +38,8 @@ helm install \
|
|||
```
|
||||
|
||||
Follow the `helm install` output to finish setting up Grafana OnCall backend and Grafana OnCall frontend plugin e.g.
|
||||
```
|
||||
|
||||
```bash
|
||||
👋 Your Grafana OnCall instance has been successfully deployed
|
||||
|
||||
❗ Set up a DNS record for your domain (use A Record and "@" to point a root domain to the IP address)
|
||||
|
|
@ -73,6 +78,7 @@ Follow the `helm install` output to finish setting up Grafana OnCall backend and
|
|||
## Configuration
|
||||
|
||||
You can edit values.yml to make changes to the helm chart configuration and re-deploy the release with the following command:
|
||||
|
||||
```bash
|
||||
helm upgrade \
|
||||
--install \
|
||||
|
|
@ -87,7 +93,7 @@ helm upgrade \
|
|||
|
||||
You can set up Slack connection via following variables:
|
||||
|
||||
```
|
||||
```yaml
|
||||
oncall:
|
||||
slack:
|
||||
enabled: true
|
||||
|
|
@ -103,7 +109,7 @@ oncall:
|
|||
|
||||
To set up Telegram tokem and webhook url use:
|
||||
|
||||
```
|
||||
```yaml
|
||||
oncall:
|
||||
telegram:
|
||||
enabled: true
|
||||
|
|
@ -112,13 +118,14 @@ oncall:
|
|||
```
|
||||
|
||||
### Set up external access
|
||||
|
||||
Grafana OnCall can be connected to the external monitoring systems or grafana deployed to the other cluster.
|
||||
Nginx Ingress Controller and Cert Manager charts are included in the helm chart with the default configuration.
|
||||
If you set the DNS A Record pointing to the external IP address of the installation with the Hostname matching base_url parameter, https will be automatically set up. If grafana is enabled in the chart values, it will also be available on https://<base_url>/grafana/. See the details in `helm install` output.
|
||||
If you set the DNS A Record pointing to the external IP address of the installation with the Hostname matching base_url parameter, https will be automatically set up. If grafana is enabled in the chart values, it will also be available on `https://<base_url>/grafana/`. See the details in `helm install` output.
|
||||
|
||||
To use a different ingress controller or tls certificate management system, set the following values to false and edit ingress settings
|
||||
|
||||
```
|
||||
```yaml
|
||||
ingress-nginx:
|
||||
enabled: false
|
||||
|
||||
|
|
@ -132,18 +139,36 @@ ingress:
|
|||
cert-manager.io/issuer: "letsencrypt-prod"
|
||||
```
|
||||
|
||||
### Use PostgreSQL instead of MySQL
|
||||
|
||||
It is possible to use PostgreSQL instead of MySQL. To do so, set mariadb.enabled to `false`,
|
||||
postgresql.enabled to `true` and database.type to `postgresql`.
|
||||
|
||||
```yaml
|
||||
mariadb:
|
||||
enabled: false
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
|
||||
database:
|
||||
type: postgresql
|
||||
```
|
||||
|
||||
### Connect external MySQL
|
||||
|
||||
It is recommended to use the managed MySQL 5.7 database provided by your cloud provider
|
||||
Make sure to create the database with the following parameters before installing this chart
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE DATABASE oncall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
To use an external MySQL instance set mysql.enabled to `false` and configure the `externalMysql` parameters.
|
||||
```
|
||||
To use an external MySQL instance set mariadb.enabled to `false` and configure the `externalMysql` parameters.
|
||||
|
||||
```yaml
|
||||
mariadb:
|
||||
enabled: true
|
||||
enabled: false
|
||||
|
||||
# Make sure to create the database with the following parameters:
|
||||
# CREATE DATABASE oncall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
|
@ -155,13 +180,42 @@ externalMysql:
|
|||
password:
|
||||
```
|
||||
|
||||
### Connect external PostgreSQL
|
||||
|
||||
To use an external PostgreSQL instance set mariadb.enabled to `false`,
|
||||
postgresql.enabled to `false`, database.type to `postgresql` and configure
|
||||
the `externalPostgresql` parameters.
|
||||
|
||||
```yaml
|
||||
mariadb:
|
||||
enabled: false
|
||||
|
||||
postgresql:
|
||||
enabled: false
|
||||
|
||||
database:
|
||||
type: postgresql
|
||||
|
||||
# Make sure to create the database with the following parameters:
|
||||
# CREATE DATABASE oncall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
externalPostgresql:
|
||||
host:
|
||||
port:
|
||||
db_name:
|
||||
user:
|
||||
password:
|
||||
existingSecret: ""
|
||||
passwordKey: password
|
||||
```
|
||||
|
||||
### Connect external RabbitMQ
|
||||
|
||||
Option 1. Install RabbitMQ separately into the cluster using the [official documentation](https://www.rabbitmq.com/kubernetes/operator/operator-overview.html)
|
||||
Option 2. Use managed solution such as [CloudAMPQ](https://www.cloudamqp.com/)
|
||||
|
||||
To use an external RabbitMQ instance set rabbitmq.enabled to `false` and configure the `externalRabbitmq` parameters.
|
||||
```
|
||||
|
||||
```yaml
|
||||
rabbitmq:
|
||||
enabled: false # Disable the RabbitMQ dependency from the release
|
||||
|
||||
|
|
@ -170,12 +224,18 @@ externalRabbitmq:
|
|||
port:
|
||||
user:
|
||||
password:
|
||||
protocol:
|
||||
vhost:
|
||||
existingSecret: ""
|
||||
passwordKey: password
|
||||
usernameKey: username
|
||||
```
|
||||
|
||||
### Connect external Redis
|
||||
|
||||
To use an external Redis instance set redis.enabled to `false` and configure the `externalRedis` parameters.
|
||||
```
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
enabled: false # Disable the Redis dependency from the release
|
||||
|
||||
|
|
@ -185,7 +245,8 @@ externalRedis:
|
|||
```
|
||||
|
||||
## Update
|
||||
```shell
|
||||
|
||||
```bash
|
||||
# Add & upgrade the repository
|
||||
helm repo add grafana https://grafana.github.io/helm-charts
|
||||
helm repo update
|
||||
|
|
@ -203,19 +264,23 @@ helm upgrade \
|
|||
After re-deploying, please also update the Grafana OnCall plugin on the plugin version page. See [Grafana docs](https://grafana.com/docs/grafana/latest/administration/plugin-management/#update-a-plugin) for more info on updating Grafana plugins.
|
||||
|
||||
## Uninstall
|
||||
|
||||
### Uninstalling the helm chart
|
||||
|
||||
```bash
|
||||
helm delete release-oncall
|
||||
```
|
||||
|
||||
### Clean up PVC's
|
||||
|
||||
```bash
|
||||
kubectl delete pvc data-release-oncall-mariadb-0 data-release-oncall-rabbitmq-0 \
|
||||
redis-data-release-oncall-redis-master-0 redis-data-release-oncall-redis-replicas-0 \
|
||||
redis-data-release-oncall-redis-replicas-1 redis-data-release-oncall-redis-replicas-2
|
||||
```
|
||||
|
||||
|
||||
### Clean up secrets
|
||||
|
||||
```bash
|
||||
kubectl delete secrets certificate-tls release-oncall-cert-manager-webhook-ca release-oncall-ingress-nginx-admission
|
||||
```
|
||||
|
|
|
|||
BIN
helm/oncall/charts/postgresql-11.9.10.tgz
Normal file
BIN
helm/oncall/charts/postgresql-11.9.10.tgz
Normal file
Binary file not shown.
|
|
@ -37,7 +37,7 @@
|
|||
Issue the one-time token to connect Grafana OnCall backend and Grafana OnCall plugin by running these commands:
|
||||
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "oncall.name" . }},app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=engine" -o jsonpath="{.items[0].metadata.name}")
|
||||
kubectl exec -it $POD_NAME -- bash -c "python manage.py issue_invite_for_the_frontend --override"
|
||||
kubectl exec -it $POD_NAME --namespace {{ .Release.Namespace }} -- bash -c "python manage.py issue_invite_for_the_frontend --override"
|
||||
|
||||
Fill the Grafana OnCall Backend URL:
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@
|
|||
value: "True"
|
||||
- name: UWSGI_LISTEN
|
||||
value: "1024"
|
||||
{{- end }}
|
||||
- name: BROKER_TYPE
|
||||
value: {{ .Values.broker.type | default "rabbitmq" }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.oncall.slack.env" -}}
|
||||
{{- if .Values.oncall.slack.enabled -}}
|
||||
|
|
@ -36,12 +38,12 @@
|
|||
- name: SLACK_SIGNING_SECRET
|
||||
value: {{ .Values.oncall.slack.signingSecret | default "" | quote }}
|
||||
- name: SLACK_INSTALL_RETURN_REDIRECT_HOST
|
||||
value: "https://{{ .Values.base_url }}"
|
||||
value: {{ .Values.oncall.slack.redirectHost | default (printf "https://%s" .Values.base_url) | quote }}
|
||||
{{- else -}}
|
||||
- name: FEATURE_SLACK_INTEGRATION_ENABLED
|
||||
value: {{ .Values.oncall.slack.enabled | toString | title | quote }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.oncall.telegram.env" -}}
|
||||
{{- if .Values.oncall.telegram.enabled -}}
|
||||
|
|
@ -55,7 +57,36 @@
|
|||
- name: FEATURE_TELEGRAM_INTEGRATION_ENABLED
|
||||
value: {{ .Values.oncall.telegram.enabled | toString | title | quote }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.oncall.twilio.env" -}}
|
||||
{{- with .Values.oncall.twilio -}}
|
||||
{{- if .accountSid }}
|
||||
- name: TWILIO_ACCOUNT_SID
|
||||
value: {{ .accountSid | quote }}
|
||||
{{- end -}}
|
||||
{{- if .authToken }}
|
||||
- name: TWILIO_AUTH_TOKEN
|
||||
value: {{ .authToken | quote }}
|
||||
{{- end -}}
|
||||
{{- if .phoneNumber }}
|
||||
- name: TWILIO_NUMBER
|
||||
value: {{ .phoneNumber | quote }}
|
||||
{{- end -}}
|
||||
{{- if .verifySid }}
|
||||
- name: TWILIO_VERIFY_SERVICE_SID
|
||||
value: {{ .verifySid | quote }}
|
||||
{{- end -}}
|
||||
{{- if .apiKeySid }}
|
||||
- name: TWILIO_API_KEY_SID
|
||||
value: {{ .apiKeySid | quote }}
|
||||
{{- end -}}
|
||||
{{- if .apiKeySecret }}
|
||||
- name: TWILIO_API_KEY_SECRET
|
||||
value: {{ .apiKeySecret | quote }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.celery.env" -}}
|
||||
{{- if .Values.celery.worker_queue }}
|
||||
|
|
@ -78,7 +109,7 @@
|
|||
- name: CELERY_WORKER_SHUTDOWN_INTERVAL
|
||||
value: {{ .Values.celery.worker_shutdown_interval }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.mysql.env" -}}
|
||||
- name: MYSQL_HOST
|
||||
|
|
@ -121,8 +152,8 @@
|
|||
{{- end -}}
|
||||
|
||||
{{- define "snippet.mysql.db" -}}
|
||||
{{- if and (not .Values.mariadb.enabled) .Values.externalMysql.db -}}
|
||||
{{- required "externalMysql.db is required if not mariadb.enabled" .Values.externalMysql.db | quote}}
|
||||
{{- if and (not .Values.mariadb.enabled) .Values.externalMysql.db_name -}}
|
||||
{{- required "externalMysql.db is required if not mariadb.enabled" .Values.externalMysql.db_name | quote}}
|
||||
{{- else -}}
|
||||
"oncall"
|
||||
{{- end -}}
|
||||
|
|
@ -130,20 +161,97 @@
|
|||
|
||||
{{- define "snippet.mysql.user" -}}
|
||||
{{- if and (not .Values.mariadb.enabled) .Values.externalMysql.user -}}
|
||||
{{- .Values.externalMysql.user | quote}}
|
||||
{{- .Values.externalMysql.user | quote }}
|
||||
{{- else -}}
|
||||
"root"
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.postgresql.env" -}}
|
||||
- name: DATABASE_TYPE
|
||||
value: {{ .Values.database.type }}
|
||||
- name: DATABASE_HOST
|
||||
value: {{ include "snippet.postgresql.host" . }}
|
||||
- name: DATABASE_PORT
|
||||
value: {{ include "snippet.postgresql.port" . }}
|
||||
- name: DATABASE_NAME
|
||||
value: {{ include "snippet.postgresql.db" . }}
|
||||
- name: DATABASE_USER
|
||||
value: {{ include "snippet.postgresql.user" . }}
|
||||
- name: DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "snippet.postgresql.password.secret.name" . }}
|
||||
key: {{ include "snippet.postgresql.password.secret.key" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "snippet.postgresql.password.secret.name" -}}
|
||||
{{- if and (not .Values.postgresql.enabled) .Values.externalPostgresql.password -}}
|
||||
{{ include "oncall.fullname" . }}-postgresql-external
|
||||
{{- else if and (not .Values.postgresql.enabled) .Values.externalPostgresql.existingSecret -}}
|
||||
{{ .Values.externalPostgresql.existingSecret }}
|
||||
{{- else -}}
|
||||
{{ include "oncall.postgresql.fullname" . }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.postgresql.password.secret.key" -}}
|
||||
{{- if and (not .Values.postgresql.enabled) .Values.externalPostgresql.passwordKey -}}
|
||||
{{ .Values.externalPostgresql.passwordKey }}
|
||||
{{- else -}}
|
||||
"postgres-password"
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.postgresql.host" -}}
|
||||
{{- if and (not .Values.postgresql.enabled) .Values.externalPostgresql.host -}}
|
||||
{{- required "externalPostgresql.host is required if not postgresql.enabled" .Values.externalPostgresql.host | quote }}
|
||||
{{- else -}}
|
||||
{{ include "oncall.postgresql.fullname" . }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.postgresql.port" -}}
|
||||
{{- if and (not .Values.mariadb.enabled) .Values.externalPostgresql.port -}}
|
||||
{{- required "externalPostgresql.port is required if not postgresql.enabled" .Values.externalPostgresql.port | quote }}
|
||||
{{- else -}}
|
||||
"5432"
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.postgresql.db" -}}
|
||||
{{- if and (not .Values.postgresql.enabled) .Values.externalPostgresql.db -}}
|
||||
{{- required "externalPostgresql.db is required if not postgresql.enabled" .Values.externalPostgresql.db | quote}}
|
||||
{{- else -}}
|
||||
"oncall"
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.postgresql.user" -}}
|
||||
{{- if and (not .Values.postgresql.enabled) .Values.externalPostgresql.user -}}
|
||||
{{- .Values.externalPostgresql.user | quote}}
|
||||
{{- else -}}
|
||||
"postgres"
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.rabbitmq.env" -}}
|
||||
{{- if eq .Values.broker.type "rabbitmq" -}}
|
||||
{{- if and (not .Values.rabbitmq.enabled) (not .Values.externalRabbitmq.existingSecret) (not .Values.externalRabbitmq.usernameKey) .Values.externalRabbitmq.user }}
|
||||
- name: RABBITMQ_USERNAME
|
||||
value: {{ include "snippet.rabbitmq.user" . }}
|
||||
{{- else if and (not .Values.rabbitmq.enabled) .Values.externalRabbitmq.existingSecret .Values.externalRabbitmq.usernameKey (not .Values.externalRabbitmq.user) }}
|
||||
- name: RABBITMQ_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "snippet.rabbitmq.password.secret.name" . }}
|
||||
key: {{ .Values.externalRabbitmq.usernameKey }}
|
||||
{{- end }}
|
||||
- name: RABBITMQ_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "snippet.rabbitmq.password.secret.name" . }}
|
||||
key: rabbitmq-password
|
||||
key: {{ include "snippet.rabbitmq.password.secret.key" . }}
|
||||
- name: RABBITMQ_HOST
|
||||
value: {{ include "snippet.rabbitmq.host" . }}
|
||||
- name: RABBITMQ_PORT
|
||||
|
|
@ -153,6 +261,7 @@
|
|||
- name: RABBITMQ_VHOST
|
||||
value: {{ include "snippet.rabbitmq.vhost" . }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.rabbitmq.user" -}}
|
||||
{{- if and (not .Values.rabbitmq.enabled) .Values.externalRabbitmq.user -}}
|
||||
|
|
@ -197,11 +306,21 @@
|
|||
{{- define "snippet.rabbitmq.password.secret.name" -}}
|
||||
{{- if and (not .Values.rabbitmq.enabled) .Values.externalRabbitmq.password -}}
|
||||
{{ include "oncall.fullname" . }}-rabbitmq-external
|
||||
{{- else if and (not .Values.rabbitmq.enabled) .Values.externalRabbitmq.existingSecret -}}
|
||||
{{ .Values.externalRabbitmq.existingSecret }}
|
||||
{{- else -}}
|
||||
{{ include "oncall.rabbitmq.fullname" . }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.rabbitmq.password.secret.key" -}}
|
||||
{{- if and (not .Values.rabbitmq.enabled) .Values.externalRabbitmq.passwordKey -}}
|
||||
{{ .Values.externalRabbitmq.passwordKey }}
|
||||
{{- else -}}
|
||||
rabbitmq-password
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "snippet.redis.host" -}}
|
||||
{{- if and (not .Values.redis.enabled) .Values.externalRedis.host -}}
|
||||
{{- required "externalRedis.host is required if not redis.enabled" .Values.externalRedis.host | quote }}
|
||||
|
|
@ -247,7 +366,7 @@
|
|||
key: smtp-password
|
||||
- name: EMAIL_USE_TLS
|
||||
value: {{ .Values.oncall.smtp.tls | toString | title | quote }}
|
||||
- name: DEFAULT_FROM_EMAIL
|
||||
- name: EMAIL_FROM_ADDRESS
|
||||
value: {{ .Values.oncall.smtp.fromEmail | quote }}
|
||||
{{- else -}}
|
||||
- name: FEATURE_EMAIL_INTEGRATION_ENABLED
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ Create the name of the service account to use
|
|||
{{- printf "%s-%s" .Release.Name "mariadb" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Generate the fullname of postgresql subchart */}}
|
||||
{{- define "oncall.postgresql.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name "postgresql" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "oncall.grafana.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name "grafana" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
|
@ -81,6 +86,31 @@ Create the name of the service account to use
|
|||
{{- end }}
|
||||
|
||||
{{- define "oncall.mariadb.wait-for-db" }}
|
||||
- name: wait-for-db
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command: ['sh', '-c', "until (python manage.py migrate --check); do echo Waiting for database migrations; sleep 2; done"]
|
||||
securityContext:
|
||||
{{ toYaml .Values.init.securityContext| nindent 4}}
|
||||
env:
|
||||
{{- include "snippet.oncall.env" . | nindent 4 }}
|
||||
{{- include "snippet.mysql.env" . | nindent 4 }}
|
||||
{{- include "snippet.rabbitmq.env" . | nindent 4 }}
|
||||
{{- include "snippet.redis.env" . | nindent 4 }}
|
||||
{{- if .Values.env }}
|
||||
{{- if (kindIs "map" .Values.env) }}
|
||||
{{- range $key, $value := .Values.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value }}
|
||||
{{- end -}}
|
||||
{{/* support previous schema */}}
|
||||
{{- else }}
|
||||
{{- toYaml .Values.env | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "oncall.postgresql.wait-for-db" }}
|
||||
- name: wait-for-db
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
|
|
@ -89,7 +119,7 @@ Create the name of the service account to use
|
|||
{{ toYaml .Values.init.securityContext| nindent 4}}
|
||||
env:
|
||||
{{- include "snippet.oncall.env" . | nindent 12 }}
|
||||
{{- include "snippet.mysql.env" . | nindent 12 }}
|
||||
{{- include "snippet.postgresql.env" . | nindent 12 }}
|
||||
{{- include "snippet.rabbitmq.env" . | nindent 12 }}
|
||||
{{- include "snippet.redis.env" . | nindent 12 }}
|
||||
{{- if .Values.env }}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,12 @@ spec:
|
|||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
initContainers:
|
||||
{{- include "oncall.mariadb.wait-for-db" . | indent 8 }}
|
||||
{{- if eq .Values.database.type "mysql" }}
|
||||
{{- include "oncall.mariadb.wait-for-db" . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.database.type "postgresql" }}
|
||||
{{- include "oncall.postgresql.wait-for-db" . | indent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
|
|
@ -42,11 +47,24 @@ spec:
|
|||
{{- include "snippet.oncall.slack.env" . | nindent 12 }}
|
||||
{{- include "snippet.oncall.telegram.env" . | nindent 12 }}
|
||||
{{- include "snippet.oncall.smtp.env" . | nindent 12 }}
|
||||
{{- if eq .Values.database.type "mysql" }}
|
||||
{{- include "snippet.mysql.env" . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.database.type "postgresql" }}
|
||||
{{- include "snippet.postgresql.env" . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- include "snippet.rabbitmq.env" . | nindent 12 }}
|
||||
{{- include "snippet.redis.env" . | nindent 12 }}
|
||||
{{- if .Values.env }}
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- if (kindIs "map" .Values.env) }}
|
||||
{{- range $key, $value := .Values.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value }}
|
||||
{{- end -}}
|
||||
{{/* support previous schema */}}
|
||||
{{- else }}
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.celery.livenessProbe.enabled }}
|
||||
livenessProbe:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ spec:
|
|||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
initContainers:
|
||||
{{- include "oncall.mariadb.wait-for-db" . | indent 8 }}
|
||||
{{- if eq .Values.database.type "mysql" }}
|
||||
{{- include "oncall.mariadb.wait-for-db" . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.database.type "postgresql" }}
|
||||
{{- include "oncall.postgresql.wait-for-db" . | indent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
|
|
@ -48,11 +53,25 @@ spec:
|
|||
{{- include "snippet.oncall.slack.env" . | nindent 12 }}
|
||||
{{- include "snippet.oncall.telegram.env" . | nindent 12 }}
|
||||
{{- include "snippet.oncall.smtp.env" . | nindent 12 }}
|
||||
{{- include "snippet.oncall.twilio.env" . | nindent 12 }}
|
||||
{{- if eq .Values.database.type "mysql" }}
|
||||
{{- include "snippet.mysql.env" . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.database.type "postgresql" }}
|
||||
{{- include "snippet.postgresql.env" . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- include "snippet.rabbitmq.env" . | nindent 12 }}
|
||||
{{- include "snippet.redis.env" . | nindent 12 }}
|
||||
{{- if .Values.env }}
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- if (kindIs "map" .Values.env) }}
|
||||
{{- range $key, $value := .Values.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value }}
|
||||
{{- end -}}
|
||||
{{/* support previous schema */}}
|
||||
{{- else }}
|
||||
{{- toYaml .Values.env | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
|
|
|
|||
|
|
@ -35,16 +35,30 @@ spec:
|
|||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
{{- if eq .Values.database.type "mysql" }}
|
||||
- |
|
||||
until (nc -vz $MYSQL_HOST $MYSQL_PORT);
|
||||
do
|
||||
echo "waiting for MySQL"; sleep 1;
|
||||
done
|
||||
python manage.py migrate
|
||||
{{- else if eq .Values.database.type "postgresql" }}
|
||||
- |
|
||||
until (nc -vz $DATABASE_HOST $DATABASE_PORT);
|
||||
do
|
||||
echo "waiting for PostgreSQL"; sleep 1;
|
||||
done
|
||||
python manage.py migrate
|
||||
{{- end }}
|
||||
env:
|
||||
{{- include "snippet.oncall.env" . | nindent 12 }}
|
||||
{{- include "snippet.oncall.smtp.env" . | nindent 12 }}
|
||||
{{- if eq .Values.database.type "mysql" }}
|
||||
{{- include "snippet.mysql.env" . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- if eq .Values.database.type "postgresql" }}
|
||||
{{- include "snippet.postgresql.env" . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- include "snippet.rabbitmq.env" . | nindent 12 }}
|
||||
{{- include "snippet.redis.env" . | nindent 12 }}
|
||||
{{- if .Values.env }}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ data:
|
|||
MIRAGE_CIPHER_IV: {{ randAlphaNum 40 | b64enc | quote }}
|
||||
|
||||
---
|
||||
{{ if not .Values.mariadb.enabled -}}
|
||||
{{ if and (not .Values.mariadb.enabled) (eq .Values.database.type "mysql") -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
|
|
@ -21,14 +21,14 @@ data:
|
|||
mariadb-root-password: {{ required "externalMysql.password is required if not mariadb.enabled" .Values.externalMysql.password | b64enc | quote }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if not .Values.rabbitmq.enabled -}}
|
||||
{{ if and (eq .Values.broker.type "rabbitmq") (not .Values.rabbitmq.enabled) (not .Values.externalRabbitmq.existingSecret) -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "oncall.fullname" . }}-rabbitmq-external
|
||||
type: Opaque
|
||||
data:
|
||||
rabbitmq-password: {{ required "externalRabbitmq.password is required if not rabbitmq.enabled" .Values.externalRabbitmq.password | b64enc | quote }}
|
||||
rabbitmq-password: {{ required "externalRabbitmq.password is required if not rabbitmq.enabled and not externalRabbitmq.existingSecret" .Values.externalRabbitmq.password | b64enc | quote }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if not .Values.redis.enabled -}}
|
||||
|
|
@ -50,3 +50,13 @@ type: Opaque
|
|||
data:
|
||||
smtp-password: {{ required "oncall.smtp.password is required if oncall.smtp.enabled" .Values.oncall.smtp.password | b64enc | quote }}
|
||||
{{- end }}
|
||||
---
|
||||
{{ if and (not .Values.postgresql.enabled) (eq .Values.database.type "postgresql") (not .Values.externalPostgresql.existingSecret) -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "oncall.fullname" . }}-postgresql-external
|
||||
type: Opaque
|
||||
data:
|
||||
postgres-password: {{ required "externalPostgresql.password is required if not postgresql.enabled and not externalPostgresql.existingSecret" .Values.externalPostgresql.password | b64enc | quote }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ oncall:
|
|||
# requests comming from Slack.
|
||||
# api.slack.com/apps/<yourApp> -> Basic Information -> App Credentials -> Signing Secret
|
||||
signingSecret: ~
|
||||
# OnCall external URL
|
||||
redirectHost: ~
|
||||
telegram:
|
||||
enabled: false
|
||||
token: ~
|
||||
|
|
@ -96,13 +98,27 @@ oncall:
|
|||
password: ~
|
||||
tls: ~
|
||||
fromEmail: ~
|
||||
twilio:
|
||||
# Twilio account SID/username to allow OnCall to send SMSes and make phone calls
|
||||
accountSid: ""
|
||||
# Twilio password to allow OnCall to send SMSes and make calls
|
||||
authToken: ""
|
||||
# Number from which you will receive calls and SMS (NOTE: must be quoted, otherwise would be rendered as float value)
|
||||
phoneNumber: ""
|
||||
# SID of Twilio service for number verification. You can create a service in Twilio web interface.
|
||||
# twilio.com -> verify -> create new service
|
||||
verifySid: ""
|
||||
# Twilio API key SID/username to allow OnCall to send SMSes and make phone calls
|
||||
apiKeySid: ""
|
||||
# Twilio API key secret/password to allow OnCall to send SMSes and make phone calls
|
||||
apiKeySecret: ""
|
||||
|
||||
# Whether to run django database migrations automatically
|
||||
migrate:
|
||||
enabled: true
|
||||
|
||||
# Additional env variables to add to deployments
|
||||
env: []
|
||||
env: {}
|
||||
|
||||
# Enable ingress object for external access to the resources
|
||||
ingress:
|
||||
|
|
@ -111,7 +127,7 @@ ingress:
|
|||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
cert-manager.io/issuer: "letsencrypt-prod"
|
||||
tls:
|
||||
tls:
|
||||
- hosts:
|
||||
- "{{ .Values.base_url }}"
|
||||
secretName: certificate-tls
|
||||
|
|
@ -153,6 +169,10 @@ cert-manager:
|
|||
- 8.8.8.8
|
||||
- 1.1.1.1
|
||||
|
||||
database:
|
||||
# can be either mysql or postgresql
|
||||
type: mysql
|
||||
|
||||
# MySQL is included into this release for the convenience.
|
||||
# It is recommended to host it separately from this release
|
||||
# Set mariadb.enabled = false and configure externalMysql
|
||||
|
|
@ -182,12 +202,36 @@ externalMysql:
|
|||
user:
|
||||
password:
|
||||
|
||||
# PostgreSQL is included into this release for the convenience.
|
||||
# It is recommended to host it separately from this release
|
||||
# Set postgresql.enabled = false and configure externalPostgresql
|
||||
postgresql:
|
||||
enabled: false
|
||||
auth:
|
||||
database: oncall
|
||||
|
||||
# Make sure to create the database with the following parameters:
|
||||
# CREATE DATABASE oncall CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
externalPostgresql:
|
||||
host:
|
||||
port:
|
||||
db_name:
|
||||
user:
|
||||
password:
|
||||
# use an existing secret for the database password
|
||||
existingSecret: ""
|
||||
# the key in the secret containing the database password
|
||||
passwordKey: password
|
||||
|
||||
# RabbitMQ is included into this release for the convenience.
|
||||
# It is recommended to host it separately from this release
|
||||
# Set rabbitmq.enabled = false and configure externalRabbitmq
|
||||
rabbitmq:
|
||||
enabled: true
|
||||
|
||||
broker:
|
||||
type: rabbitmq
|
||||
|
||||
externalRabbitmq:
|
||||
host:
|
||||
port:
|
||||
|
|
@ -195,6 +239,12 @@ externalRabbitmq:
|
|||
password:
|
||||
protocol:
|
||||
vhost:
|
||||
# use an existing secret for the rabbitmq password
|
||||
existingSecret: ""
|
||||
# the key in the secret containing the rabbitmq password
|
||||
passwordKey: password
|
||||
# the key in the secret containing the rabbitmq username
|
||||
usernameKey: username
|
||||
|
||||
# Redis is included into this release for the convenience.
|
||||
# It is recommended to host it separately from this release
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue