From c2546460f369e6817c77d539a9359acd3b8ebc2c Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Wed, 2 Nov 2022 10:36:12 -0600 Subject: [PATCH 1/5] Add user and org id to log to make troubleshooting easier --- engine/engine/middlewares.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/engine/engine/middlewares.py b/engine/engine/middlewares.py index 90c1e130..367bf69c 100644 --- a/engine/engine/middlewares.py +++ b/engine/engine/middlewares.py @@ -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 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") From 538e28e739e612587f1775d3742ea371c3e59102 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Wed, 2 Nov 2022 10:47:26 -0600 Subject: [PATCH 2/5] Check user attribute --- engine/engine/middlewares.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/engine/middlewares.py b/engine/engine/middlewares.py index 367bf69c..cf5878ad 100644 --- a/engine/engine/middlewares.py +++ b/engine/engine/middlewares.py @@ -28,7 +28,7 @@ class RequestTimeLoggingMiddleware(MiddlewareMixin): 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 request.user and request.user.id: + 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} " From 5b29ab92ead9786015c99e87652add8b442eafa0 Mon Sep 17 00:00:00 2001 From: Michael Derynck Date: Wed, 2 Nov 2022 16:10:41 -0600 Subject: [PATCH 3/5] Migration for regions and organizations --- .../migrations/0004_auto_20221025_0316.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 engine/apps/user_management/migrations/0004_auto_20221025_0316.py diff --git a/engine/apps/user_management/migrations/0004_auto_20221025_0316.py b/engine/apps/user_management/migrations/0004_auto_20221025_0316.py new file mode 100644 index 00000000..8d1b15c8 --- /dev/null +++ b/engine/apps/user_management/migrations/0004_auto_20221025_0316.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.15 on 2022-10-25 03:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0003_user_hide_phone_number'), + ] + + operations = [ + migrations.CreateModel( + name='Region', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=300)), + ('slug', models.CharField(max_length=50, unique=True)), + ('oncall_backend_url', models.URLField(null=True)), + ], + ), + migrations.AddField( + model_name='organization', + name='region_slug', + field=models.CharField(default=None, max_length=300, null=True), + ), + migrations.AddField( + model_name='organization', + name='migration_destination', + field=models.ForeignKey(db_column='migration_destination_slug', default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='regions', to='user_management.region', to_field='slug'), + ), + ] From c01068898a3e2a7a2b5cd038506354e860b1aabb Mon Sep 17 00:00:00 2001 From: David van der Spek Date: Thu, 3 Nov 2022 08:31:00 +0100 Subject: [PATCH 4/5] Helm: allow for using existing secret for RabbitMQ (#761) * init rabbitmq existing secret Signed-off-by: David van der Spek * bump chart Signed-off-by: David van der Spek Signed-off-by: David van der Spek --- helm/oncall/Chart.yaml | 2 +- helm/oncall/README.md | 5 +++++ helm/oncall/templates/_env.tpl | 20 +++++++++++++++++++- helm/oncall/templates/secrets.yaml | 4 ++-- helm/oncall/values.yaml | 6 ++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/helm/oncall/Chart.yaml b/helm/oncall/Chart.yaml index fd0483fd..b1b7e18a 100644 --- a/helm/oncall/Chart.yaml +++ b/helm/oncall/Chart.yaml @@ -8,7 +8,7 @@ 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.7 +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 diff --git a/helm/oncall/README.md b/helm/oncall/README.md index d64e6c89..92fcd911 100644 --- a/helm/oncall/README.md +++ b/helm/oncall/README.md @@ -224,6 +224,11 @@ externalRabbitmq: port: user: password: + protocol: + vhost: + existingSecret: "" + passwordKey: password + usernameKey: username ``` ### Connect external Redis diff --git a/helm/oncall/templates/_env.tpl b/helm/oncall/templates/_env.tpl index 89d1a52c..855ce711 100644 --- a/helm/oncall/templates/_env.tpl +++ b/helm/oncall/templates/_env.tpl @@ -237,13 +237,21 @@ {{- 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 @@ -298,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 }} diff --git a/helm/oncall/templates/secrets.yaml b/helm/oncall/templates/secrets.yaml index 0997c93d..dfd7cdb2 100644 --- a/helm/oncall/templates/secrets.yaml +++ b/helm/oncall/templates/secrets.yaml @@ -21,14 +21,14 @@ data: mariadb-root-password: {{ required "externalMysql.password is required if not mariadb.enabled" .Values.externalMysql.password | b64enc | quote }} {{- end }} --- -{{ if and (eq .Values.broker.type "rabbitmq") (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 -}} diff --git a/helm/oncall/values.yaml b/helm/oncall/values.yaml index 5c30878d..3de0e122 100644 --- a/helm/oncall/values.yaml +++ b/helm/oncall/values.yaml @@ -239,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 From e9df5ab5c9cd7620fec8843b87240a2a3fa5594c Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Thu, 3 Nov 2022 14:45:13 +0000 Subject: [PATCH 5/5] Send emails from .grafana.net for cloud (#766) --- engine/apps/email/tasks.py | 6 +- engine/apps/email/tests/test_notify_user.py | 69 +++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/engine/apps/email/tasks.py b/engine/apps/email/tasks.py index d9054745..403d4cdc 100644 --- a/engine/apps/email/tasks.py +++ b/engine/apps/email/tasks.py @@ -76,9 +76,13 @@ 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 recipient_list = [user.email] + if settings.LICENSE == settings.CLOUD_LICENSE_NAME: + email_from = "oncall@{}.grafana.net".format(user.organization.stack_slug) + else: + email_from = live_settings.EMAIL_HOST_USER + connection = get_connection( host=live_settings.EMAIL_HOST, port=live_settings.EMAIL_PORT, diff --git a/engine/apps/email/tests/test_notify_user.py b/engine/apps/email/tests/test_notify_user.py index f5a10601..321d420a 100644 --- a/engine/apps/email/tests/test_notify_user.py +++ b/engine/apps/email/tests/test_notify_user.py @@ -155,3 +155,72 @@ 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 +def test_notify_user_from_email_oss( + 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.LICENSE = settings.OPEN_SOURCE_LICENSE_NAME + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + settings.EMAIL_HOST = "test" + settings.EMAIL_HOST_USER = "test@test.com" + + organization = make_organization(stack_slug="example") + 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 mail.outbox[0].from_email == "test@test.com" + + +@pytest.mark.django_db +def test_notify_user_from_email_cloud( + 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.LICENSE = settings.CLOUD_LICENSE_NAME + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + settings.EMAIL_HOST = "test" + + organization = make_organization(stack_slug="slug") + 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 mail.outbox[0].from_email == "oncall@slug.grafana.net"