Merge branch 'dev' of https://github.com/grafana/oncall into dev

This commit is contained in:
Ildar Iskhakov 2022-06-13 13:38:02 +03:00
commit 385738dd48
28 changed files with 232 additions and 238 deletions

2
.gitignore vendored
View file

@ -3,7 +3,7 @@
*.pyc
venv
.env
.env-hobby
.env_hobby
.vscode
dump.rdb
.idea

View file

@ -53,9 +53,6 @@ export $(grep -v '^#' .env | xargs -0)
# Hint: there is a known issue with uwsgi. It's not used in the local dev environment. Feel free to comment it in `engine/requirements.txt`.
cd engine && pip install -r requirements.txt
# Create folder for database
mkdir sqlite_data
# Migrate the DB:
python manage.py migrate
@ -107,7 +104,7 @@ python manage.py issue_invite_for_the_frontend --override
OnCall API URL:
http://host.docker.internal:8000
OnCall Invitation Token (Single use token to connect Grafana instance):
Invitation Token (Single use token to connect Grafana instance):
Response from the invite generator command (check above)
Grafana URL (URL OnCall will use to talk to Grafana instance):

View file

@ -1,6 +1,6 @@
<img width="400px" src="docs/img/logo.png">
Developer-friendly, incident response management with brilliant Slack integration.
Developer-friendly, incident response with brilliant Slack integration.
<img width="60%" src="screenshot.png">

View file

@ -12,7 +12,7 @@ services:
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: local_dev_pwd
MYSQL_ROOT_PASSWORD: empty
MYSQL_DATABASE: oncall_local_dev
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
@ -42,13 +42,13 @@ services:
mysql-to-create-grafana-db:
image: mariadb:10.2
platform: linux/x86_64
command: bash -c "mysql -h mysql -uroot -plocal_dev_pwd -e 'CREATE DATABASE IF NOT EXISTS grafana CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"
command: bash -c "mysql -h mysql -uroot -pempty -e 'CREATE DATABASE IF NOT EXISTS grafana CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'"
depends_on:
mysql:
condition: service_healthy
grafana:
image: "grafana/grafana:8.5.5"
image: "grafana/grafana:9.0.0-beta3"
restart: always
mem_limit: 500m
cpus: 0.5
@ -56,7 +56,7 @@ services:
GF_DATABASE_TYPE: mysql
GF_DATABASE_HOST: mysql
GF_DATABASE_USER: root
GF_DATABASE_PASSWORD: local_dev_pwd
GF_DATABASE_PASSWORD: empty
GF_SECURITY_ADMIN_USER: oncall
GF_SECURITY_ADMIN_PASSWORD: oncall
GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: grafana-oncall-app

View file

@ -143,7 +143,7 @@ services:
- with_grafana
grafana:
image: "grafana/grafana:8.3.2"
image: "grafana/grafana:9.0.0-beta3"
mem_limit: 500m
ports:
- 3000:3000

View file

@ -113,20 +113,7 @@ class ChannelFilter(OrderedModel):
return satisfied_filter
def is_satisfying(self, raw_request_data, title, message=None):
AlertReceiveChannel = apps.get_model("alerts", "AlertReceiveChannel")
return (
self.is_default
or self.check_filter(json.dumps(raw_request_data))
or self.check_filter(str(title))
or
# Special case for Amazon SNS
(
self.check_filter(str(message))
if self.alert_receive_channel.integration == AlertReceiveChannel.INTEGRATION_AMAZON_SNS
else False
)
)
return self.is_default or self.check_filter(json.dumps(raw_request_data)) or self.check_filter(str(title))
def check_filter(self, value):
return re.search(self.filtering_term, value)

View file

@ -2,7 +2,7 @@ import pytest
from apps.alerts.incident_appearance.templaters import AlertSlackTemplater
from apps.alerts.models import AlertGroup
from apps.integrations.metadata.configuration import grafana
from config_integrations import grafana
@pytest.mark.django_db

View file

@ -10,9 +10,9 @@ from apps.alerts.incident_appearance.templaters import (
AlertWebTemplater,
)
from apps.alerts.models import Alert, AlertReceiveChannel
from apps.integrations.metadata.configuration import grafana
from common.jinja_templater import jinja_template_env
from common.utils import getattrd
from config_integrations import grafana
@pytest.mark.django_db

View file

@ -1,99 +0,0 @@
# Main
enabled = True
title = "Amazon SNS"
slug = "amazon_sns"
short_description = None
is_displayed_on_web = True
description = None
is_featured = False
is_able_to_autoresolve = True
is_demo_alert_enabled = True
description = None
# Default templates
slack_title = """\
{% if payload|length == 0 -%}
{% set title = payload.get("AlarmName", "Alert") %}
{%- else -%}
{% set title = "Alert" %}
{%- endif %}
*<{{ grafana_oncall_link }}|#{{ grafana_oncall_incident_id }} {{ title }}>* via {{ integration_name }}
{% if source_link %}
(*<{{ source_link }}|source>*)
{%- endif %}"""
slack_message = """\
{% if payload|length == 1 and "message" in payload -%}
{{ payload.get("message", "Non-JSON payload received. Please make sure you publish monitoring Alarms to SNS, not logs: https://docs.amixr.io/#/integrations/amazon_sns") }}
{%- else -%}
*State* {{ payload.get("NewStateValue", "NO") }}
Region: {{ payload.get("Region", "Undefined") }}
_Description_: {{ payload.get("AlarmDescription", "Undefined") }}
{%- endif %}
"""
slack_image_url = None
web_title = """\
{% if payload|length == 0 -%}
{{ payload.get("AlarmName", "Alert")}}
{%- else -%}
Alert
{%- endif %}"""
web_message = """\
{% if payload|length == 1 and "message" in payload -%}
{{ payload.get("message", "Non-JSON payload received. Please make sure you publish monitoring Alarms to SNS, not logs: https://docs.amixr.io/#/integrations/amazon_sns") }}
{%- else -%}
**State** {{ payload.get("NewStateValue", "NO") }}
Region: {{ payload.get("Region", "Undefined") }}
*Description*: {{ payload.get("AlarmDescription", "Undefined") }}
{%- endif %}
"""
web_image_url = slack_image_url
sms_title = web_title
phone_call_title = web_title
email_title = web_title
email_message = "{{ payload|tojson_pretty }}"
telegram_title = sms_title
telegram_message = """\
{% if payload|length == 1 and "message" in payload -%}
{{ payload.get("message", "Non-JSON payload received. Please make sure you publish monitoring Alarms to SNS, not logs: https://docs.amixr.io/#/integrations/amazon_sns") }}
{%- else -%}
<b>State</b> {{ payload.get("NewStateValue", "NO") }}
Region: {{ payload.get("Region", "Undefined") }}
<i>Description</i>: {{ payload.get("AlarmDescription", "Undefined") }}
{%- endif %}
"""
telegram_image_url = slack_image_url
source_link = """\
{% if payload|length == 0 -%}
{% if payload.get("Trigger", {}).get("Namespace") == "AWS/ElasticBeanstalk" -%}
https://console.aws.amazon.com/elasticbeanstalk/home?region={{ payload.get("TopicArn").split(":")[3] }}
{%- else -%}
https://console.aws.amazon.com/cloudwatch//home?region={{ payload.get("TopicArn").split(":")[3] }}
{%- endif %}
{%- endif %}"""
grouping_id = web_title
resolve_condition = """\
{{ payload.get("NewStateValue", "") == "OK" }}
"""
acknowledge_condition = None
group_verbose_name = web_title
example_payload = {"foo": "bar"}

View file

@ -56,7 +56,11 @@ class OpenAlertAppearanceDialogStep(
raw_request_data = json.dumps(alert_group.alerts.first().raw_request_data, sort_keys=True, indent=4)
# This is a special case for amazon sns notifications in str format CHEKED
if alert_group.channel.integration == AlertReceiveChannel.INTEGRATION_AMAZON_SNS and raw_request_data == "{}":
if (
AlertReceiveChannel.INTEGRATION_AMAZON_SNS is not None
and alert_group.channel.integration == AlertReceiveChannel.INTEGRATION_AMAZON_SNS
and raw_request_data == "{}"
):
raw_request_data = alert_group.alerts.first().message
raw_request_data_chunks = [

View file

@ -0,0 +1,66 @@
# Main
enabled = True
title = "Elastalert"
slug = "elastalert"
short_description = "Elastic"
is_displayed_on_web = True
description = None
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 }} Incident>* via {{ integration_name }}
{% if source_link %}
(*<{{ source_link }}|source>*)
{%- endif %}"""
slack_message = "```{{ payload|tojson_pretty }}```"
slack_image_url = None
web_title = "Incident"
web_message = """\
```
{{ payload|tojson_pretty }}
```
"""
web_image_url = slack_image_url
sms_title = web_title
phone_call_title = sms_title
email_title = web_title
email_message = "{{ payload|tojson_pretty }}"
telegram_title = sms_title
telegram_message = "<code>{{ payload|tojson_pretty }}</code>"
telegram_image_url = slack_image_url
source_link = None
grouping_id = '{{ payload.get("alert_uid", "")}}'
resolve_condition = """\
{%- if "is_amixr_heartbeat_restored" in payload -%}
{# We don't know the payload format from your integration. #}
{# The heartbeat alerts will go here so we check for our own key #}
{{ payload["is_amixr_heartbeat_restored"] }}
{%- else -%}
{{ payload.get("state", "").upper() == "OK" }}'
{%- endif %}"""
acknowledge_condition = None
group_verbose_name = "Incident"
example_payload = {"message": "This alert was sent by user for the demonstration purposes"}

View file

@ -0,0 +1,65 @@
# Main
enabled = True
title = "Kapacitor"
slug = "kapacitor"
short_description = "InfluxDB"
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("id", "Title undefined (check Slack Title Template)") }}>* via {{ integration_name }}
{% if source_link %}
(*<{{ source_link }}|source>*)
{%- endif %}"""
slack_message = """\
```{{ payload|tojson_pretty }}```
"""
slack_image_url = None
web_title = '{{ payload.get("id", "Title undefined (check Web Title Template)") }}'
web_message = """\
```
{{ payload|tojson_pretty }}
```
"""
web_image_url = slack_image_url
sms_title = web_title
phone_call_title = web_title
email_title = web_title
email_message = slack_message
telegram_title = sms_title
telegram_message = "<code>{{ payload|tojson_pretty }}</code>"
telegram_image_url = slack_image_url
source_link = None
grouping_id = '{{ payload.get("id", "") }}'
resolve_condition = '{{ payload.get("level", "").startswith("OK") }}'
acknowledge_condition = None
group_verbose_name = '{{ payload.get("id", "") }}'
example_payload = {
"id": "TestAlert",
"message": "This alert was sent by user for the demonstration purposes",
"data": "{foo: bar}",
}

View file

@ -1,34 +0,0 @@
#!/bin/bash
export DJANGO_SETTINGS_MODULE=settings.all_in_one
generate_value_if_not_exist ()
{
if [ ! -f /etc/app/secret_data/$1 ]; then
touch /etc/app/secret_data/$1
base64 /dev/urandom | head -c $2 > /etc/app/secret_data/$1
fi
export $1=$(cat /etc/app/secret_data/$1)
}
generate_value_if_not_exist SECRET_KEY 75
generate_value_if_not_exist MIRAGE_SECRET_KEY 75
generate_value_if_not_exist MIRAGE_CIPHER_IV 16
export BASE_URL=http://localhost:8000
echo "Starting redis in the background"
# Redis will dump the changes to the volume every 60 seconds if at least 1 key changed
redis-server --daemonize yes --save 60 1 --dir /etc/app/redis_data/
echo "Running migrations"
python manage.py migrate
echo "Start celery"
python manage.py start_celery &
# Postponing token issuing to make sure it's the last record in the console.
bash -c 'sleep 10; python manage.py issue_invite_for_the_frontend --override' &
echo "Starting server"
python manage.py runserver 0.0.0.0:8000 --noreload

View file

@ -428,15 +428,16 @@ FEATURE_EXTRA_MESSAGING_BACKENDS_ENABLED = getenv_boolean("FEATURE_EXTRA_MESSAGI
EXTRA_MESSAGING_BACKENDS = []
INSTALLED_ONCALL_INTEGRATIONS = [
"apps.integrations.metadata.configuration.alertmanager",
"apps.integrations.metadata.configuration.grafana",
"apps.integrations.metadata.configuration.grafana_alerting",
"apps.integrations.metadata.configuration.formatted_webhook",
"apps.integrations.metadata.configuration.webhook",
"apps.integrations.metadata.configuration.amazon_sns",
"apps.integrations.metadata.configuration.heartbeat",
"apps.integrations.metadata.configuration.inbound_email",
"apps.integrations.metadata.configuration.maintenance",
"apps.integrations.metadata.configuration.manual",
"apps.integrations.metadata.configuration.slack_channel",
"config_integrations.alertmanager",
"config_integrations.grafana",
"config_integrations.grafana_alerting",
"config_integrations.formatted_webhook",
"config_integrations.webhook",
"config_integrations.kapacitor",
"config_integrations.elastalert",
"config_integrations.heartbeat",
"config_integrations.inbound_email",
"config_integrations.maintenance",
"config_integrations.manual",
"config_integrations.slack_channel",
]

View file

@ -1,6 +1,9 @@
import os
import sys
# Workaround to use pymysql instead of mysqlclient
import pymysql
from .base import * # noqa
SECRET_KEY = os.environ.get("SECRET_KEY", "osMsNM0PqlRHBlUvqmeJ7+ldU3IUETCrY9TrmiViaSmInBHolr1WUlS0OFS4AHrnnkp1vp9S9z1")
@ -10,14 +13,26 @@ MIRAGE_SECRET_KEY = os.environ.get(
)
MIRAGE_CIPHER_IV = os.environ.get("MIRAGE_CIPHER_IV", "tZZa+60zTZO2NRcS")
# Primary database must have the name "default"
pymysql.install_as_MySQLdb()
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "sqlite_data/db.sqlite3"), # noqa
"ENGINE": "django.db.backends.mysql",
"NAME": os.environ.get("MYSQL_DB_NAME", "oncall_local_dev"),
"USER": os.environ.get("MYSQL_USER", "root"),
"PASSWORD": os.environ.get("MYSQL_PASSWORD"),
"HOST": os.environ.get("MYSQL_HOST", "127.0.0.1"),
"PORT": os.environ.get("MYSQL_PORT", "3306"),
"OPTIONS": {
"charset": "utf8mb4",
"connect_timeout": 1,
},
},
}
os.environ.setdefault("OSS", "True")
INSTALLED_APPS += ["apps.oss_installation"] # noqa
TESTING = "pytest" in sys.modules or "unittest" in sys.modules
READONLY_DATABASES = {}

View file

@ -1,7 +1,8 @@
.delete_configuration_button {
margin-top: 20px;
}
.command-line {
width: 100%;
}
.info-block {
margin-bottom: 24px;
margin-top: 24px;
}

View file

@ -19,6 +19,7 @@ import cn from 'classnames/bind';
import CopyToClipboard from 'react-copy-to-clipboard';
import { OnCallAppSettings } from 'types';
import Block from 'components/GBlock/Block';
import Text from 'components/Text/Text';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import logo from 'img/logo.svg';
@ -34,23 +35,20 @@ const cx = cn.bind(styles);
interface Props extends PluginConfigPageProps<AppPluginMeta<OnCallAppSettings>> {}
export const PluginConfigPage = (props: Props) => {
const grafanaUrlDefault = getItem('grafanaUrl') || window.location.origin;
const { plugin } = props;
const [onCallApiUrl, setOnCallApiUrl] = useState<string>(getItem('onCallApiUrl'));
const [onCallInvitationToken, setOnCallInvitationToken] = useState<string>();
const [grafanaUrl, setGrafanaUrl] = useState<string>(window.location.origin);
const [grafanaUrl, setGrafanaUrl] = useState<string>(grafanaUrlDefault);
const [pluginConfigLoading, setPluginConfigLoading] = useState<boolean>(true);
const [pluginStatusOk, setPluginStatusOk] = useState<boolean>();
const [pluginStatusMessage, setPluginStatusMessage] = useState<string>();
const [isSelfHostedInstall, setIsSelfHostedInstall] = useState<boolean>(true);
const [retrySync, setRetrySync] = useState<boolean>(false);
const [showConfirmationModal, setShowConfirmationModal] = useState<boolean>(false);
const configurePlugin = () => {
setShowConfirmationModal(true);
};
const setupPlugin = useCallback(async () => {
setItem('onCallApiUrl', onCallApiUrl);
setShowConfirmationModal(false);
setItem('grafanaUrl', grafanaUrl);
await getBackendSrv().post(`/api/plugins/grafana-oncall-app/settings`, {
enabled: true,
pinned: true,
@ -218,35 +216,50 @@ export const PluginConfigPage = (props: Props) => {
<img alt="Grafana OnCall Logo" src={logo} width={18} />
</p>
)}
<p>{'Plugin <-> backend connection status'}</p>
<pre>
<Text type="link">{pluginStatusMessage}</Text>
</pre>
{isSelfHostedInstall ? (
<div>
<p>{'Plugin <-> backend connection status:'}</p>
<HorizontalGroup>
{/* <p>{'Plugin <-> backend connection status'}</p>
<pre>
<Text type="link">{pluginStatusMessage}</Text>
</pre>
</pre> */}
{retrySync && (
<Button variant="primary" onClick={startSync} size="md">
Retry
</Button>
)}
{isSelfHostedInstall ? (
<WithConfirm title="Are you sure to delete OnCall plugin configuration?">
<Button
variant="destructive"
onClick={resetPlugin}
size="md"
className={cx('delete_configuration_button')}
>
<Button variant="destructive" onClick={resetPlugin} size="md">
Remove current configuration
</Button>
</WithConfirm>
</div>
) : (
<Label>This is a cloud managed configuration.</Label>
)}
) : (
<Label>This is a cloud managed configuration.</Label>
)}{' '}
</HorizontalGroup>
</>
) : (
<React.Fragment>
<Legend>Configure Grafana OnCall</Legend>
<p>This page will help you to connect OnCall backend and OnCall Grafana plugin 👋</p>
<p>
<p>1. Launch backend</p>
<VerticalGroup>
<Text type="secondary">
- Talk to the OnCall team in the #grafana-oncall channel at{' '}
Run hobby, dev or production backend:{' '}
<a href="https://github.com/grafana/oncall#getting-started">
<Text type="link">getting started.</Text>
</a>
</Text>
</VerticalGroup>
<Block withBackground className={cx('info-block')}>
<Text type="secondary">
Need help?
<br />- Talk to the OnCall team in the #grafana-oncall channel at{' '}
<a href="https://slack.grafana.com/">
<Text type="link">Slack</Text>
</a>
@ -259,17 +272,8 @@ export const PluginConfigPage = (props: Props) => {
<Text type="link">GitHub Issues</Text>
</a>
</Text>
</p>
<p>1. Launch backend</p>
<p>
<Text type="secondary">
Run hobby, dev or production backend:{' '}
<a href="https://github.com/grafana/oncall#getting-started">
<Text type="link">getting started.</Text>
</a>
</Text>
</p>
<p></p>
</Block>
<p>2. Conect the backend and the plugin </p>
<p>{'Plugin <-> backend connection status:'}</p>
<pre>
@ -292,42 +296,29 @@ Seek for such a line: “Your invite token: <<LONG TOKEN>> , use it in the Graf
<Field
label="OnCall backend URL"
description="It should be reachable from Grafana. Possible options:
http://host.docker.internal:8000 (if you run backend in the docker locally)
http://localhost:8000
..."
description={
<Text>
It should be rechable from Grafana. Possible options: <br />
http://host.docker.internal:8000 (if you run backend in the docker locally)
<br />
http://localhost:8000 <br />
...
</Text>
}
>
<Input id="onCallApiUrl" onChange={handleApiUrlChange} defaultValue={onCallApiUrl} />
</Field>
<Field label="Grafana Url" description="URL of the current Grafana instance. ">
<Field label="Grafana URL" description="URL of the current Grafana instance. ">
<Input id="grafanaUrl" onChange={handleGrafanaUrlChange} defaultValue={grafanaUrl} />
</Field>
{/* <WithConfirm title="Admin API key for OnCall will be created in Grafana. Continue?" confirmText="Continue"> */}
<Button
variant="primary"
onClick={configurePlugin}
onClick={setupPlugin}
disabled={!onCallApiUrl || !onCallInvitationToken || !grafanaUrl}
size="md"
>
Connect
</Button>
{/* </WithConfirm> */}
{showConfirmationModal && (
<Modal
isOpen
title="Admin API key for OnCall will be created in Grafana. Continue?"
onDismiss={() => setShowConfirmationModal(false)}
>
<HorizontalGroup>
<Button variant="primary" onClick={setupPlugin}>
Continue
</Button>
<Button variant="secondary" onClick={() => setShowConfirmationModal(false)}>
Cancel
</Button>
</HorizontalGroup>
</Modal>
)}
</React.Fragment>
)}
</div>

View file

@ -289,7 +289,7 @@ const CloudPage = observer((props: CloudPageProps) => {
</Button>
) : (
<Button variant="primary" onClick={syncUsers} icon="sync">
Sync users
Sync users (Editors and Admins)
</Button>
)}
</HorizontalGroup>
@ -351,7 +351,7 @@ const CloudPage = observer((props: CloudPageProps) => {
<Icon name="bell" className={cx('block-icon')} size="lg" /> SMS and phone call notifications
</Text.Title>
<Text type="secondary">Users matched between OSS and Cloud OnCall currently unavialable.</Text>
<Text type="secondary">Users matched between OSS and Cloud OnCall currently unavailable.</Text>
</VerticalGroup>
</Block>
</VerticalGroup>