refactor(alerts): core QAlerts and WebhookSigner on QQQ RC.5 (#15 part 1) #17
No files matched your search
@@ -4,7 +4,7 @@ This file provides guidance to Codex when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**QBit Alerts**: QQQ QBit: alerts and manage-by-exception (monitors, alerts, subscriptions, channels and notifications as standard QQQ tables). Design: [QRun-IO/qqq#1090](https://github.com/QRun-IO/qqq/pull/1090). Phases P1 to P3 are implemented; see README.md for the remaining phases and the core SPI stand-in in `spi/`.
|
||||
**QBit Alerts**: QQQ QBit: alerts and manage-by-exception (monitors, alerts, subscriptions, channels and notifications as standard QQQ tables). Design: [QRun-IO/qqq#1090](https://github.com/QRun-IO/qqq/pull/1090). Phases P1 to P3 are implemented; see README.md for the remaining phases and the notification compatibility types in `spi/`.
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -25,7 +25,7 @@ Packages:
|
||||
- `processes`: the record actions (`AlertsAction`, `AlertsActionSteps`) and the scheduled steps.
|
||||
- `home`: the Alerts home and `AlertSummaryLoader`.
|
||||
- `api`: the bell and record-panel routes.
|
||||
- `spi`: the stand-in for the core producer SPI.
|
||||
- `spi`: notification types and their AlertLink dependency retained for binary compatibility until core RC.6; alerts use core RC.5.
|
||||
|
||||
## Naming (owner rule)
|
||||
|
||||
@@ -44,7 +44,7 @@ Packages:
|
||||
## Dependencies
|
||||
|
||||
- Java 21.
|
||||
- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.4` ahead of it, as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
|
||||
- Parent `com.kingsrook:qbit-build-parent:2.0.0` (qqq 4.0.0). The default-active `qqq-snapshot` profile imports `qqq-bom-pom:4.1.0-RC.5` ahead of it, as in qbit-quick-search (ADR-0007). Remove that activation when the parent moves to 4.1.
|
||||
- Tests: JUnit 5, AssertJ.
|
||||
|
||||
## Release
|
||||
|
||||
+7
-2
@@ -4,11 +4,16 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
### Changed
|
||||
- Build against the released QQQ 4.1.0-RC.5 BOM. Alert types and sink registration now use core's alerts SPI; host `QAlerts.raise/resolve/heartbeat` calls reach the installed qbit through core instance metadata (qbit-alerts#15, F3).
|
||||
- Removed alert SPI copies. Retained `spi.NotificationSignal`, `NotificationAudience`, `NotificationCategory`, `NotificationSeverity`, `AlertLink`, and the binary-compatible `AlertsRuntime.sink().notify(...)` entry point for qbit-easypost-tracking 0.1.x until QRun-IO/qqq#1125 ships in RC.6.
|
||||
- Webhook signing delegates to core `WebhookSigner`/`WebhookSecret` (qbit-alerts#15, F11). Headers, `v1,` signatures and `whsec_` encoding are unchanged. A thin adapter preserves legacy keys shorter than core's 16-byte minimum using equivalent HMAC zero-padding.
|
||||
|
||||
### Added
|
||||
- Optional external channels:
|
||||
- Slack (an Incoming Webhook, Block Kit)
|
||||
- Microsoft Teams (a Workflows webhook, an Adaptive Card)
|
||||
- a generic webhook, signed with Standard Webhooks headers through a stand-in for core's `WebhookSigner` (QRun-IO/qqq#1109)
|
||||
- a generic webhook, signed with Standard Webhooks headers through core's `WebhookSigner` (QRun-IO/qqq#1109)
|
||||
- PagerDuty (Events API v2: trigger, acknowledge and resolve)
|
||||
- an Alertmanager sink (`/api/v2/alerts`, with the qqq labels and the origin guard)
|
||||
- Prometheus Alertmanager ingest at `POST /qqq/v1/alerts/ingest/alertmanager`. It takes a bearer token, creates EXTERNAL types with notifications off, and keeps a loop guard.
|
||||
@@ -43,5 +48,5 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
- The top-level Alerts app and its home dashboard, plus `AlertSummaryLoader` for link-only summaries.
|
||||
- `/qqq/v1/alerts` routes for the Next UI bell and record panel.
|
||||
- Record security, through three key types (`alerts_user`, `alerts_permission`, `alerts_readable_table`).
|
||||
- A stand-in for the core producer SPI (QRun-IO/qqq#1108), to be removed when that RC ships.
|
||||
- Core alert producer SPI integration (QRun-IO/qqq#1108), plus temporary notification SPI compatibility until RC.6 (QRun-IO/qqq#1125).
|
||||
- Repository scaffold: `AlertsQBitConfig` and `AlertsQBitProducer` (table prefix `alerts_`, permissions `alerts.*`), build, CI and repository standard files.
|
||||
@@ -51,7 +51,7 @@ Grant `getNotificationAdminPermissionNames()` to authors. Viewers' permissions i
|
||||
## Requirements
|
||||
|
||||
- Java 21.
|
||||
- QQQ 4.1 (built against `4.1.0-RC.4`).
|
||||
- QQQ 4.1 (built against `4.1.0-RC.5`).
|
||||
- An RDBMS backend.
|
||||
- A scheduler, for monitors and email delivery.
|
||||
- A messaging provider, for email.
|
||||
@@ -84,16 +84,18 @@ new AlertsQBitProducer()
|
||||
|
||||
## Raising alerts
|
||||
|
||||
Until the core producer SPI ([QRun-IO/qqq#1108](https://github.com/QRun-IO/qqq/pull/1108)) is in a released QQQ RC, the SPI types are a stand-in in `com.kingsrook.qbits.alerts.spi` (see its `package-info.java`). In the meantime, call the qbit's sink directly:
|
||||
Use the producer SPI from `com.kingsrook.qqq.backend.core.alerts`, released in QQQ 4.1.0-RC.5 ([QRun-IO/qqq#1108](https://github.com/QRun-IO/qqq/pull/1108)). `AlertsQBitProducer` registers the qbit's sink through core's `AlertsInstanceMetaData`, so host producers need only core:
|
||||
|
||||
```java
|
||||
AlertsRuntime.get().sink().raise(new AlertSignal("cdn.purge.dead")
|
||||
QAlerts.raise(new AlertSignal("cdn.purge.dead")
|
||||
.withDedupKey("provider:" + providerId)
|
||||
.withTitle("3 CDN purges failed after retries") // external-safe: no personal data
|
||||
.withRecordRefs(AlertRecordRef.of("cdn_purge_request", ids)));
|
||||
```
|
||||
|
||||
Once the RC ships, producers call `QAlerts.raise(...)` from core and need no dependency on this qbit.
|
||||
`QAlerts.resolve(...)` and `QAlerts.heartbeat(...)` reach the same sink. Declare types with core's `QAlertTypeMetaData`. Calls through `QAlerts.raise/resolve` defer to transaction commit when a transaction is present; without this qbit installed, core uses its logging sink.
|
||||
|
||||
User notifications remain on `AlertsRuntime.get(qInstance).sink().notify(...)`: core's notification API ([QRun-IO/qqq#1125](https://github.com/QRun-IO/qqq/pull/1125)) is expected in RC.6 and is not in RC.5. The four `spi.Notification*` types and `spi.AlertLink` retain their binary signatures for released qbit-easypost-tracking 0.1.x. Use core `AlertLink` for alert signals and `spi.AlertLink` for notification signals until that migration.
|
||||
|
||||
## Endpoints (for the Next UI)
|
||||
|
||||
@@ -122,7 +124,7 @@ Each external channel is an `alerts_channel` row that an admin creates. Its secr
|
||||
|
||||
How each one sends:
|
||||
|
||||
- **Webhook** bodies are `{type, subject, severity, link, alerts: [...]}`, signed with the Standard Webhooks headers (`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64 HMAC-SHA256 of id.timestamp.body>`). This is the shared format from the qbit-webhooks assessment (QRun-IO/qqq#1105). The signer is a stand-in until core's `WebhookSigner` (QRun-IO/qqq#1109) is released.
|
||||
- **Webhook** bodies are `{type, subject, severity, link, alerts: [...]}`, signed by core's `WebhookSigner` with the Standard Webhooks headers (`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64 HMAC-SHA256 of id.timestamp.body>`). Both raw UTF-8 and `whsec_` base64 secrets keep the same wire format. Core's `WebhookSecret` requires 16 key bytes; a thin adapter zero-pads legacy shorter keys to 16 bytes, which is HMAC-SHA256-equivalent and preserves their signatures. New secrets should use `WebhookSecret.generate()`.
|
||||
- **PagerDuty** gets one event per alert: `trigger`, `acknowledge` or `resolve`, with `dedup_key` set to the type and key.
|
||||
- **Alertmanager** alerts carry the labels `qqq_type`, `qqq_group`, `qqq_key` and `qqq_origin="qbit-alerts"`. Ingest ignores alerts with that origin, so nothing loops.
|
||||
- **Responses:** 2xx is sent. 408, 425, 429, 5xx and network errors are retried. Any other 4xx fails permanently.
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
The design for this qbit is tracked in [QRun-IO/qqq#1090](https://github.com/QRun-IO/qqq/pull/1090). This repository implements it. Phases P1 to P3 are built (see README.md); P4 to P6 remain.
|
||||
|
||||
Deviations from the design, and why:
|
||||
- **The core SPI is a stand-in** (`spi/`) until QRun-IO/qqq#1108 is released.
|
||||
- **Alerts use core's RC.5 SPI** (QRun-IO/qqq#1108), including sink registration in core instance metadata. Only notification types and their `AlertLink` signature dependency remain in `spi/`, preserving released producers until QRun-IO/qqq#1125 ships in RC.6.
|
||||
- **Visibility checks 1 to 3 (section 7.2)** are enforced by record security locks on session keys (`alerts_permission`, `alerts_readable_table`), not by a pre-query customizer. QQQ has no pre-query customizer, and locks also apply to counts, exports and the API.
|
||||
- **Heartbeats** are stored in an extra table, `alerts_heartbeat`.
|
||||
- **Claims** are leases, not atomic conditional updates (see README.md, Known limits).
|
||||
|
||||
@@ -138,8 +138,9 @@ notification admins. The home stays the only Alerts dashboard.
|
||||
|
||||
## 6. Producer SPI
|
||||
|
||||
It extends the core alerts SPI (`com.kingsrook.qqq.backend.core.alerts`) through a core PR to qqq
|
||||
`develop`, for the next 4.1 RC:
|
||||
The notification API below is planned in [QRun-IO/qqq#1125](https://github.com/QRun-IO/qqq/pull/1125)
|
||||
for 4.1.0-RC.6. The baseline is 4.1.0-RC.5, which ships the core alert SPI
|
||||
(`com.kingsrook.qqq.backend.core.alerts`) but does **not** yet provide `QAlerts.notify`:
|
||||
|
||||
```java
|
||||
QAlerts.notify(new NotificationSignal("Your export failed")
|
||||
@@ -156,8 +157,12 @@ QAlerts.notify(new NotificationSignal("Your export failed")
|
||||
- The guarantees match `raise`: it never throws or blocks, it waits for the commit when a transaction is
|
||||
open, and bad input is counted.
|
||||
- `NotificationAudience`: `allUsers()`, `roles(...)`, `permission(p)`, `users(...)`, `user(id)`.
|
||||
- Until that RC ships, the qbit carries the same classes in its `spi` stand-in package, and hosts call
|
||||
`AlertsRuntime.get(qInstance).sink().notify(...)`.
|
||||
- Until RC.6 ships, the qbit keeps `spi.NotificationSignal`, `NotificationAudience`,
|
||||
`NotificationCategory`, `NotificationSeverity` and `AlertLink`, and hosts call
|
||||
`AlertsRuntime.get(qInstance).sink().notify(...)`. Their binary signatures remain unchanged for
|
||||
released qbit-easypost-tracking 0.1.x. The return type of `sink()` remains `QbitAlertsSink`;
|
||||
there is no local sink interface. This direct notification path queues immediately; the
|
||||
transaction-aware `QAlerts.notify` guarantees above describe the planned core API.
|
||||
|
||||
### 6.1 Automatic sources
|
||||
|
||||
|
||||
@@ -158,14 +158,14 @@
|
||||
<profiles>
|
||||
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
|
||||
<!-- imports the released 4.1 BOM, this profile is active by default and -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.4 ahead of the parent's 4.0.0 BOM. -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.5 ahead of the parent's 4.0.0 BOM. -->
|
||||
<profile>
|
||||
<id>qqq-snapshot</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.4</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.5</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -36,7 +36,7 @@ import com.kingsrook.qbits.alerts.model.AlertsTeam;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsTeamMember;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsUserPreference;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig;
|
||||
|
||||
@@ -27,11 +27,11 @@ import com.kingsrook.qbits.alerts.engine.QbitAlertsSink;
|
||||
import com.kingsrook.qbits.alerts.metadata.AlertsTableMetaDataHelper;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsChannel;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsMonitor;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.DetailLevel;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
|
||||
@@ -276,8 +276,9 @@ public class AlertsRuntime
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The sink producers send signals to (until the core SPI ships, hosts call
|
||||
** it directly: AlertsRuntime.get().sink().raise(...)).
|
||||
** The runtime sink. Alert producers use core QAlerts; notification
|
||||
** producers keep calling sink().notify(spi.NotificationSignal) until RC.6.
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
*******************************************************************************/
|
||||
public QbitAlertsSink sink()
|
||||
{
|
||||
|
||||
@@ -45,13 +45,13 @@ import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.SubscriberType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsRecordRef;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsSubscription;
|
||||
import com.kingsrook.qbits.alerts.notifications.NotificationService;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationCategory;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSeverity;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
@@ -25,8 +25,8 @@ import java.util.Map;
|
||||
import com.kingsrook.qbits.alerts.home.HomeHtml;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsAlert;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.DeliveryKind;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.HealthStatus;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Resolution;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsUserPreference;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
@@ -327,7 +327,7 @@ public class DeliveryWorker
|
||||
.withTitle("Alert channel \"" + channel.getValueString("label") + "\" is failing")
|
||||
.withSummary(failures + " deliveries in a row failed.")
|
||||
.withDetails(result.error())
|
||||
.withRecordRef(com.kingsrook.qbits.alerts.spi.AlertRecordRef.of(AlertsChannel.TABLE_NAME, channelId)));
|
||||
.withRecordRef(com.kingsrook.qqq.backend.core.alerts.AlertRecordRef.of(AlertsChannel.TABLE_NAME, channelId)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-55
@@ -17,36 +17,25 @@
|
||||
package com.kingsrook.qbits.alerts.delivery.channels;
|
||||
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.utils.webhooks.WebhookSecret;
|
||||
import com.kingsrook.qqq.backend.core.utils.webhooks.WebhookSigner;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN for core's WebhookSigner (QRun-IO/qqq#1109, the shared signer of
|
||||
** the qbit-webhooks assessment, QRun-IO/qqq#1105 D2). Same format: the
|
||||
** Standard Webhooks headers webhook-id, webhook-timestamp and
|
||||
** webhook-signature = "v1," + base64(HMAC-SHA256(key, id + "." + timestamp +
|
||||
** "." + body)); a "whsec_" secret is base64 after the prefix, any other text
|
||||
** is the raw key. Replace with com.kingsrook.qqq.backend.core.utils.webhooks
|
||||
** .WebhookSigner when the QQQ RC with #1109 ships, so receivers verify Alerts
|
||||
** and qbit-webhooks deliveries the same way.
|
||||
** Compatibility adapter for the qbit's existing webhook secret strings.
|
||||
** Core signs every request. Core requires at least 16 key bytes, whereas the
|
||||
** previous signer accepted shorter keys. HMAC-SHA256 zero-pads keys to its
|
||||
** 64-byte block size, so padding a short legacy key to 16 bytes preserves
|
||||
** its signature exactly. New secrets should use WebhookSecret.generate().
|
||||
*******************************************************************************/
|
||||
public final class StandardWebhookSigner
|
||||
{
|
||||
public static final String HEADER_ID = "webhook-id";
|
||||
public static final String HEADER_TIMESTAMP = "webhook-timestamp";
|
||||
public static final String HEADER_SIGNATURE = "webhook-signature";
|
||||
|
||||
private static final String PREFIX = "whsec_";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Signing only.
|
||||
*******************************************************************************/
|
||||
@@ -57,55 +46,36 @@ public final class StandardWebhookSigner
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The three signature headers.
|
||||
** The three core Standard Webhooks headers, preserving legacy short keys.
|
||||
*******************************************************************************/
|
||||
public static Map<String, String> headers(String messageId, Instant timestamp, byte[] body, String secret)
|
||||
{
|
||||
long seconds = timestamp.getEpochSecond();
|
||||
Map<String, String> headers = new LinkedHashMap<>();
|
||||
headers.put(HEADER_ID, messageId);
|
||||
headers.put(HEADER_TIMESTAMP, Long.toString(seconds));
|
||||
headers.put(HEADER_SIGNATURE, "v1," + sign(messageId, seconds, body, secret));
|
||||
return (headers);
|
||||
return (WebhookSigner.headers(messageId, timestamp, body, List.of(parseSecret(secret))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** base64 HMAC-SHA256 over id.timestamp.body.
|
||||
** Preserve raw UTF-8 and whsec_ base64 secrets, including legacy short keys.
|
||||
*******************************************************************************/
|
||||
public static String sign(String messageId, long timestampSeconds, byte[] body, String secret)
|
||||
{
|
||||
if(messageId == null || messageId.isEmpty() || messageId.contains(".") || messageId.chars().anyMatch(Character::isWhitespace))
|
||||
{
|
||||
throw (new IllegalArgumentException("A webhook message id is required and may not contain '.' or whitespace"));
|
||||
}
|
||||
try
|
||||
{
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(keyBytes(secret), "HmacSHA256"));
|
||||
mac.update((messageId + "." + timestampSeconds + ".").getBytes(StandardCharsets.UTF_8));
|
||||
mac.update(body == null ? new byte[0] : body);
|
||||
return (Base64.getEncoder().encodeToString(mac.doFinal()));
|
||||
}
|
||||
catch(GeneralSecurityException e)
|
||||
{
|
||||
throw (new IllegalStateException("HMAC-SHA256 is unavailable", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The key bytes of a secret's text form.
|
||||
*******************************************************************************/
|
||||
static byte[] keyBytes(String secret)
|
||||
private static WebhookSecret parseSecret(String secret)
|
||||
{
|
||||
if(secret == null || secret.isBlank())
|
||||
{
|
||||
throw (new IllegalArgumentException("A webhook signing secret is required"));
|
||||
}
|
||||
String trimmed = secret.trim();
|
||||
return (trimmed.startsWith(PREFIX) ? Base64.getDecoder().decode(trimmed.substring(PREFIX.length())) : trimmed.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] key = trimmed.startsWith(WebhookSecret.PREFIX)
|
||||
? Base64.getDecoder().decode(trimmed.substring(WebhookSecret.PREFIX.length()))
|
||||
: trimmed.getBytes(StandardCharsets.UTF_8);
|
||||
if(key.length == 0)
|
||||
{
|
||||
throw (new IllegalArgumentException("A webhook signing secret is required"));
|
||||
}
|
||||
if(key.length < WebhookSecret.MINIMUM_KEY_BYTES)
|
||||
{
|
||||
return (WebhookSecret.parse(WebhookSecret.PREFIX + Base64.getEncoder().encodeToString(Arrays.copyOf(key, WebhookSecret.MINIMUM_KEY_BYTES))));
|
||||
}
|
||||
return (WebhookSecret.parse(trimmed));
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ import com.kingsrook.qbits.alerts.model.AlertsSubscription;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsTeamMember;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsUserPreference;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
@@ -27,12 +27,12 @@ import com.kingsrook.qbits.alerts.model.AlertsGroup;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsMonitor;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertDefaultMonitor;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.DetailLevel;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertDefaultMonitor;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
|
||||
@@ -29,11 +29,11 @@ import com.kingsrook.qbits.alerts.delivery.AlertRedactor;
|
||||
import com.kingsrook.qbits.alerts.delivery.channels.ExternalChannels;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Resolution;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.DetailLevel;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
@@ -38,10 +38,10 @@ import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Resolution;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Status;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsRecordRef;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertLink;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertRecordRef;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertLink;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
|
||||
@@ -26,12 +26,12 @@ import com.kingsrook.qbits.alerts.AlertsRuntime;
|
||||
import com.kingsrook.qbits.alerts.AlertsSecurity;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsMonitor;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertLink;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertMonitorKind;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationAudience;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationCategory;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSignal;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.listeners.ProcessLifecycleListenerInterface;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertMonitorKind;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLine;
|
||||
|
||||
@@ -19,10 +19,9 @@ package com.kingsrook.qbits.alerts.engine;
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qbits.alerts.AlertsRuntime;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertResolution;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSinkInterface;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertResolution;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSinkInterface;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
|
||||
|
||||
@@ -77,19 +76,4 @@ public class AlertsSinkAdapter implements AlertSinkInterface
|
||||
runtime.sink().heartbeat(heartbeatKey, at);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void notify(NotificationSignal signal)
|
||||
{
|
||||
AlertsRuntime runtime = AlertsRuntime.find(QContext.getQInstance());
|
||||
if(runtime != null)
|
||||
{
|
||||
runtime.sink().notify(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,13 +45,13 @@ import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.MonitorStatus;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Resolution;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Status;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertLink;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertMonitorKind;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertRecordRef;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.AggregateAction;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertLink;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertMonitorKind;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
|
||||
@@ -28,11 +28,11 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import com.kingsrook.qbits.alerts.AlertsRuntime;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Resolution;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertResolution;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSinkInterface;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertResolution;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSinkInterface;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
@@ -134,10 +134,11 @@ public class QbitAlertsSink implements AlertSinkInterface
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** Queue a notification (it is written and sent by the notification
|
||||
** service on the drain thread).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void notify(NotificationSignal signal)
|
||||
{
|
||||
if(signal == null)
|
||||
|
||||
@@ -27,7 +27,7 @@ import com.kingsrook.qbits.alerts.AlertsSecurity;
|
||||
import com.kingsrook.qbits.alerts.engine.AlertsDb;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsGroup;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Status;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
|
||||
@@ -44,17 +44,17 @@ import com.kingsrook.qbits.alerts.model.AlertsEvent;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsGroup;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotification;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotificationRecipient;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.DeliveryStatus;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.EventType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.HealthStatus;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Status;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsSubscription;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.notifications.NotificationService;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
|
||||
@@ -20,13 +20,13 @@ package com.kingsrook.qbits.alerts.model;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertMonitorKind;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.DetailLevel;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationAudience;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationCategory;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertMonitorKind;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.alerts.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A monitor that an alert type ships with ("seeded"). An alerts
|
||||
** implementation inserts it once; after that it belongs to the admin, who may
|
||||
** tune or disable it. Without an implementation installed it does nothing.
|
||||
**
|
||||
** The definition's keys depend on the kind (e.g. HEARTBEAT takes heartbeatKey
|
||||
** or processName, plus expectedEverySeconds and graceSeconds; TABLE_QUERY
|
||||
** takes tableName and filter). Values must be JSON-friendly.
|
||||
*******************************************************************************/
|
||||
public class AlertDefaultMonitor implements Serializable
|
||||
{
|
||||
private String name;
|
||||
private AlertMonitorKind kind;
|
||||
private Map<String, Serializable> definition = new LinkedHashMap<>();
|
||||
private Boolean perRecord = false;
|
||||
private AlertSeverity severity;
|
||||
private Integer scheduleSeconds;
|
||||
private Integer forMinutes = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A heartbeat monitor: raise when the key has not beat within the expected
|
||||
** interval plus grace.
|
||||
*******************************************************************************/
|
||||
public static AlertDefaultMonitor heartbeat(String name, String heartbeatKey, int expectedEverySeconds, int graceSeconds)
|
||||
{
|
||||
return (new AlertDefaultMonitor()
|
||||
.withName(name)
|
||||
.withKind(AlertMonitorKind.HEARTBEAT)
|
||||
.withDefinitionValue("heartbeatKey", heartbeatKey)
|
||||
.withDefinitionValue("expectedEverySeconds", expectedEverySeconds)
|
||||
.withDefinitionValue("graceSeconds", graceSeconds));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for kind
|
||||
*******************************************************************************/
|
||||
public AlertMonitorKind getKind()
|
||||
{
|
||||
return (this.kind);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for kind
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withKind(AlertMonitorKind kind)
|
||||
{
|
||||
this.kind = kind;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for definition
|
||||
*******************************************************************************/
|
||||
public Map<String, Serializable> getDefinition()
|
||||
{
|
||||
return (this.definition);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for definition
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withDefinition(Map<String, Serializable> definition)
|
||||
{
|
||||
this.definition = definition == null ? new LinkedHashMap<>() : new LinkedHashMap<>(definition);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Set one definition value.
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withDefinitionValue(String key, Serializable value)
|
||||
{
|
||||
this.definition.put(key, value);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for perRecord
|
||||
*******************************************************************************/
|
||||
public Boolean getPerRecord()
|
||||
{
|
||||
return (this.perRecord);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for perRecord (one alert per matching record, instead of one
|
||||
** aggregate alert)
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withPerRecord(Boolean perRecord)
|
||||
{
|
||||
this.perRecord = perRecord;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for severity
|
||||
*******************************************************************************/
|
||||
public AlertSeverity getSeverity()
|
||||
{
|
||||
return (this.severity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for severity (default: the type's severity)
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withSeverity(AlertSeverity severity)
|
||||
{
|
||||
this.severity = severity;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for scheduleSeconds
|
||||
*******************************************************************************/
|
||||
public Integer getScheduleSeconds()
|
||||
{
|
||||
return (this.scheduleSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for scheduleSeconds (how often to evaluate)
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withScheduleSeconds(Integer scheduleSeconds)
|
||||
{
|
||||
this.scheduleSeconds = scheduleSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for forMinutes
|
||||
*******************************************************************************/
|
||||
public Integer getForMinutes()
|
||||
{
|
||||
return (this.forMinutes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for forMinutes (how long a condition must hold before it fires)
|
||||
*******************************************************************************/
|
||||
public AlertDefaultMonitor withForMinutes(Integer forMinutes)
|
||||
{
|
||||
this.forMinutes = forMinutes;
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** A link from an alert to where a person fixes it: a record, a table with a
|
||||
** filter, an app or widget path, or an absolute URL. Exactly one target is
|
||||
** expected, so construct these with the static factories.
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The kinds of monitor an alerts implementation evaluates. Declared in core
|
||||
** so producers can describe a seeded (default) monitor for their alert types
|
||||
** without depending on the implementation.
|
||||
*******************************************************************************/
|
||||
public enum AlertMonitorKind
|
||||
{
|
||||
/** count of rows matching a filter passes a threshold. */
|
||||
TABLE_QUERY,
|
||||
|
||||
/** an aggregate (count, sum, avg, min, max) crosses a band. */
|
||||
AGGREGATE,
|
||||
|
||||
/** nothing was seen (a heartbeat, a process success, a newest row) within an interval. */
|
||||
HEARTBEAT,
|
||||
|
||||
/** a process run failed. */
|
||||
PROCESS_FAILED,
|
||||
|
||||
/** a matching ESB event arrived. */
|
||||
ESB_EVENT,
|
||||
|
||||
/** a code check returned the currently-failing conditions. */
|
||||
CODE,
|
||||
|
||||
/** alerts ingested from an external system (e.g. Alertmanager). */
|
||||
EXTERNAL
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A record that an alert is about. The label is internal (shown in-app to
|
||||
** users who can see the record), never sent to an external channel.
|
||||
*******************************************************************************/
|
||||
public class AlertRecordRef implements Serializable
|
||||
{
|
||||
private String tableName;
|
||||
private Serializable primaryKey;
|
||||
private String label;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
*******************************************************************************/
|
||||
public AlertRecordRef()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
*******************************************************************************/
|
||||
public AlertRecordRef(String tableName, Serializable primaryKey)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
this.primaryKey = primaryKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One ref per primary key, all in the same table. Null keys are skipped.
|
||||
*******************************************************************************/
|
||||
public static List<AlertRecordRef> of(String tableName, Collection<? extends Serializable> primaryKeys)
|
||||
{
|
||||
List<AlertRecordRef> refs = new ArrayList<>();
|
||||
if(primaryKeys != null)
|
||||
{
|
||||
for(Serializable primaryKey : primaryKeys)
|
||||
{
|
||||
if(primaryKey != null)
|
||||
{
|
||||
refs.add(new AlertRecordRef(tableName, primaryKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (refs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One ref to one record.
|
||||
*******************************************************************************/
|
||||
public static AlertRecordRef of(String tableName, Serializable primaryKey)
|
||||
{
|
||||
return (new AlertRecordRef(tableName, primaryKey));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Two refs are equal when they name the same table and the same key (keys
|
||||
** compared by their string form, so 1 and "1" match).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
if(!(o instanceof AlertRecordRef that))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
return (Objects.equals(tableName, that.tableName) && Objects.equals(String.valueOf(primaryKey), String.valueOf(that.primaryKey)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return (Objects.hash(tableName, String.valueOf(primaryKey)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return (tableName + ":" + primaryKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tableName
|
||||
*******************************************************************************/
|
||||
public String getTableName()
|
||||
{
|
||||
return (this.tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tableName
|
||||
*******************************************************************************/
|
||||
public void setTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableName
|
||||
*******************************************************************************/
|
||||
public AlertRecordRef withTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for primaryKey
|
||||
*******************************************************************************/
|
||||
public Serializable getPrimaryKey()
|
||||
{
|
||||
return (this.primaryKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for primaryKey
|
||||
*******************************************************************************/
|
||||
public void setPrimaryKey(Serializable primaryKey)
|
||||
{
|
||||
this.primaryKey = primaryKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for primaryKey
|
||||
*******************************************************************************/
|
||||
public AlertRecordRef withPrimaryKey(Serializable primaryKey)
|
||||
{
|
||||
this.primaryKey = primaryKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for label
|
||||
*******************************************************************************/
|
||||
public void setLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label (internal: shown in-app only, never sent out)
|
||||
*******************************************************************************/
|
||||
public AlertRecordRef withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A producer's "this problem is gone", passed to QAlerts.resolve. Resolving
|
||||
** a key that is not open is a no-op.
|
||||
*******************************************************************************/
|
||||
public class AlertResolution implements Serializable
|
||||
{
|
||||
private String alertTypeName;
|
||||
private String dedupKey;
|
||||
private String note;
|
||||
private Instant resolvedAt;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
*******************************************************************************/
|
||||
public AlertResolution()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
*******************************************************************************/
|
||||
public AlertResolution(String alertTypeName, String dedupKey, String note)
|
||||
{
|
||||
this.alertTypeName = alertTypeName;
|
||||
this.dedupKey = dedupKey;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The dedup key to use: the one given, or the type name.
|
||||
*******************************************************************************/
|
||||
public String getEffectiveDedupKey()
|
||||
{
|
||||
return (dedupKey == null || dedupKey.isBlank() ? alertTypeName : dedupKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for alertTypeName
|
||||
*******************************************************************************/
|
||||
public String getAlertTypeName()
|
||||
{
|
||||
return (this.alertTypeName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for alertTypeName
|
||||
*******************************************************************************/
|
||||
public AlertResolution withAlertTypeName(String alertTypeName)
|
||||
{
|
||||
this.alertTypeName = alertTypeName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for dedupKey
|
||||
*******************************************************************************/
|
||||
public String getDedupKey()
|
||||
{
|
||||
return (this.dedupKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for dedupKey
|
||||
*******************************************************************************/
|
||||
public AlertResolution withDedupKey(String dedupKey)
|
||||
{
|
||||
this.dedupKey = dedupKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for note
|
||||
*******************************************************************************/
|
||||
public String getNote()
|
||||
{
|
||||
return (this.note);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for note
|
||||
*******************************************************************************/
|
||||
public AlertResolution withNote(String note)
|
||||
{
|
||||
this.note = note;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for resolvedAt
|
||||
*******************************************************************************/
|
||||
public Instant getResolvedAt()
|
||||
{
|
||||
return (this.resolvedAt);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for resolvedAt (default: when QAlerts.resolve is called)
|
||||
*******************************************************************************/
|
||||
public AlertResolution withResolvedAt(Instant resolvedAt)
|
||||
{
|
||||
this.resolvedAt = resolvedAt;
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** How urgent an alert is. Ordered from least to most severe, so
|
||||
** isAtLeast / compareTo can be used for "minimum severity" checks.
|
||||
**
|
||||
** An alert means "a person should do something". INFO is for worth-a-glance
|
||||
** items that never page anyone; external channels ignore it by default.
|
||||
*******************************************************************************/
|
||||
public enum AlertSeverity
|
||||
{
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
CRITICAL;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** True when this severity is the same as, or more severe than, the other.
|
||||
** A null other is treated as INFO (so everything is at least it).
|
||||
*******************************************************************************/
|
||||
public boolean isAtLeast(AlertSeverity other)
|
||||
{
|
||||
return (other == null || this.compareTo(other) >= 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Leniently map a severity label from another system (Prometheus, a
|
||||
** producer's own enum, etc) to an AlertSeverity. Case-insensitive; accepts
|
||||
** common synonyms ("warn", "err", "fatal", "page", "info"/"informational").
|
||||
** Returns the fallback for null, blank or unknown values.
|
||||
*******************************************************************************/
|
||||
public static AlertSeverity fromLabel(String label, AlertSeverity fallback)
|
||||
{
|
||||
if(label == null || label.isBlank())
|
||||
{
|
||||
return (fallback);
|
||||
}
|
||||
|
||||
return switch(label.trim().toLowerCase(Locale.ROOT))
|
||||
{
|
||||
case "info", "informational", "notice", "low" -> INFO;
|
||||
case "warning", "warn", "medium" -> WARNING;
|
||||
case "error", "err", "high" -> ERROR;
|
||||
case "critical", "crit", "fatal", "page", "emergency" -> CRITICAL;
|
||||
default -> fallback;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,489 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One "this is a problem" signal from a producer, passed to QAlerts.raise.
|
||||
** Raising is idempotent on (alertTypeName, dedupKey): repeats refresh one open
|
||||
** alert, they never duplicate it.
|
||||
**
|
||||
** Text fields have different reach. title and summary are external-safe: a
|
||||
** producer must write them without personal data, because they may be sent
|
||||
** to email or chat. details (and record ref labels) are internal: shown
|
||||
** in-app only, to users who can see the alert.
|
||||
**
|
||||
** Bounds: at most MAX_RECORD_REFS record refs (extras are dropped, and count
|
||||
** keeps the true total) and MAX_ATTRIBUTES attributes (extras are ignored).
|
||||
*******************************************************************************/
|
||||
public class AlertSignal implements Serializable
|
||||
{
|
||||
public static final int MAX_RECORD_REFS = 100;
|
||||
public static final int MAX_ATTRIBUTES = 20;
|
||||
|
||||
private String alertTypeName;
|
||||
private String dedupKey;
|
||||
private AlertSeverity severity;
|
||||
private String title;
|
||||
private String summary;
|
||||
private String details;
|
||||
private List<AlertRecordRef> recordRefs = new ArrayList<>();
|
||||
private List<AlertLink> links = new ArrayList<>();
|
||||
private Map<String, String> attributes = new LinkedHashMap<>();
|
||||
private Instant occurredAt;
|
||||
private Integer count;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
*******************************************************************************/
|
||||
public AlertSignal()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
*******************************************************************************/
|
||||
public AlertSignal(String alertTypeName)
|
||||
{
|
||||
this.alertTypeName = alertTypeName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The dedup key to use: the one given, or the type name (one alert per type).
|
||||
*******************************************************************************/
|
||||
public String getEffectiveDedupKey()
|
||||
{
|
||||
return (dedupKey == null || dedupKey.isBlank() ? alertTypeName : dedupKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The record count this signal describes: count when set, otherwise the
|
||||
** number of record refs (null when there are none).
|
||||
*******************************************************************************/
|
||||
public Integer getEffectiveCount()
|
||||
{
|
||||
if(count != null)
|
||||
{
|
||||
return (count);
|
||||
}
|
||||
return (recordRefs.isEmpty() ? null : recordRefs.size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for alertTypeName
|
||||
*******************************************************************************/
|
||||
public String getAlertTypeName()
|
||||
{
|
||||
return (this.alertTypeName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for alertTypeName
|
||||
*******************************************************************************/
|
||||
public void setAlertTypeName(String alertTypeName)
|
||||
{
|
||||
this.alertTypeName = alertTypeName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for alertTypeName
|
||||
*******************************************************************************/
|
||||
public AlertSignal withAlertTypeName(String alertTypeName)
|
||||
{
|
||||
this.alertTypeName = alertTypeName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for dedupKey
|
||||
*******************************************************************************/
|
||||
public String getDedupKey()
|
||||
{
|
||||
return (this.dedupKey);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for dedupKey
|
||||
*******************************************************************************/
|
||||
public void setDedupKey(String dedupKey)
|
||||
{
|
||||
this.dedupKey = dedupKey;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for dedupKey - a stable key, so repeats update one open alert.
|
||||
*******************************************************************************/
|
||||
public AlertSignal withDedupKey(String dedupKey)
|
||||
{
|
||||
this.dedupKey = dedupKey;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for severity
|
||||
*******************************************************************************/
|
||||
public AlertSeverity getSeverity()
|
||||
{
|
||||
return (this.severity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for severity
|
||||
*******************************************************************************/
|
||||
public void setSeverity(AlertSeverity severity)
|
||||
{
|
||||
this.severity = severity;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for severity (optional; defaults to the type's severity)
|
||||
*******************************************************************************/
|
||||
public AlertSignal withSeverity(AlertSeverity severity)
|
||||
{
|
||||
this.severity = severity;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for title
|
||||
*******************************************************************************/
|
||||
public String getTitle()
|
||||
{
|
||||
return (this.title);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for title
|
||||
*******************************************************************************/
|
||||
public void setTitle(String title)
|
||||
{
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for title (external-safe: no personal data)
|
||||
*******************************************************************************/
|
||||
public AlertSignal withTitle(String title)
|
||||
{
|
||||
this.title = title;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for summary
|
||||
*******************************************************************************/
|
||||
public String getSummary()
|
||||
{
|
||||
return (this.summary);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for summary
|
||||
*******************************************************************************/
|
||||
public void setSummary(String summary)
|
||||
{
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for summary (external-safe: no personal data)
|
||||
*******************************************************************************/
|
||||
public AlertSignal withSummary(String summary)
|
||||
{
|
||||
this.summary = summary;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for details
|
||||
*******************************************************************************/
|
||||
public String getDetails()
|
||||
{
|
||||
return (this.details);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for details
|
||||
*******************************************************************************/
|
||||
public void setDetails(String details)
|
||||
{
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for details (internal: in-app only, never sent externally)
|
||||
*******************************************************************************/
|
||||
public AlertSignal withDetails(String details)
|
||||
{
|
||||
this.details = details;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for recordRefs
|
||||
*******************************************************************************/
|
||||
public List<AlertRecordRef> getRecordRefs()
|
||||
{
|
||||
return (this.recordRefs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for recordRefs
|
||||
*******************************************************************************/
|
||||
public void setRecordRefs(List<AlertRecordRef> recordRefs)
|
||||
{
|
||||
this.recordRefs = new ArrayList<>();
|
||||
withRecordRefs(recordRefs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add record refs. Beyond MAX_RECORD_REFS they are dropped, and count is
|
||||
** set to the true total so the alert still says how many there were.
|
||||
*******************************************************************************/
|
||||
public AlertSignal withRecordRefs(Collection<AlertRecordRef> recordRefs)
|
||||
{
|
||||
if(recordRefs == null)
|
||||
{
|
||||
return (this);
|
||||
}
|
||||
|
||||
int total = this.recordRefs.size();
|
||||
for(AlertRecordRef recordRef : recordRefs)
|
||||
{
|
||||
if(recordRef == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
total++;
|
||||
if(this.recordRefs.size() < MAX_RECORD_REFS)
|
||||
{
|
||||
this.recordRefs.add(recordRef);
|
||||
}
|
||||
}
|
||||
|
||||
if(total > MAX_RECORD_REFS && (count == null || count < total))
|
||||
{
|
||||
count = total;
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add one record ref.
|
||||
*******************************************************************************/
|
||||
public AlertSignal withRecordRef(AlertRecordRef recordRef)
|
||||
{
|
||||
return (withRecordRefs(recordRef == null ? null : List.of(recordRef)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for links
|
||||
*******************************************************************************/
|
||||
public List<AlertLink> getLinks()
|
||||
{
|
||||
return (this.links);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for links
|
||||
*******************************************************************************/
|
||||
public void setLinks(List<AlertLink> links)
|
||||
{
|
||||
this.links = links == null ? new ArrayList<>() : new ArrayList<>(links);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add a link to where the problem is fixed.
|
||||
*******************************************************************************/
|
||||
public AlertSignal withLink(AlertLink link)
|
||||
{
|
||||
if(link != null)
|
||||
{
|
||||
this.links.add(link);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for attributes
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getAttributes()
|
||||
{
|
||||
return (this.attributes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for attributes
|
||||
*******************************************************************************/
|
||||
public void setAttributes(Map<String, String> attributes)
|
||||
{
|
||||
this.attributes = new LinkedHashMap<>();
|
||||
if(attributes != null)
|
||||
{
|
||||
attributes.forEach(this::withAttribute);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add a routing label (not displayed by default). At most MAX_ATTRIBUTES
|
||||
** keys; a new key beyond that is ignored (replacing an existing key is fine).
|
||||
*******************************************************************************/
|
||||
public AlertSignal withAttribute(String key, String value)
|
||||
{
|
||||
if(key != null && (attributes.containsKey(key) || attributes.size() < MAX_ATTRIBUTES))
|
||||
{
|
||||
attributes.put(key, value);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for occurredAt
|
||||
*******************************************************************************/
|
||||
public Instant getOccurredAt()
|
||||
{
|
||||
return (this.occurredAt);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for occurredAt
|
||||
*******************************************************************************/
|
||||
public void setOccurredAt(Instant occurredAt)
|
||||
{
|
||||
this.occurredAt = occurredAt;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for occurredAt (default: when QAlerts.raise is called)
|
||||
*******************************************************************************/
|
||||
public AlertSignal withOccurredAt(Instant occurredAt)
|
||||
{
|
||||
this.occurredAt = occurredAt;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for count
|
||||
*******************************************************************************/
|
||||
public Integer getCount()
|
||||
{
|
||||
return (this.count);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for count
|
||||
*******************************************************************************/
|
||||
public void setCount(Integer count)
|
||||
{
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for count - for aggregate alerts, e.g. "37 records".
|
||||
*******************************************************************************/
|
||||
public AlertSignal withCount(Integer count)
|
||||
{
|
||||
this.count = count;
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Where QAlerts sends signals. The default is LoggingAlertSink; an alerts
|
||||
** implementation (such as qbit-alerts) registers its own through
|
||||
** AlertsInstanceMetaData.withSink.
|
||||
**
|
||||
** Methods are called after the caller's transaction commits (or at once, when
|
||||
** there is none), on the caller's thread, so they must be fast: queue the
|
||||
** work, don't do database writes or network calls inline. Anything they
|
||||
** throw is caught, logged and counted by QAlerts; it never reaches the
|
||||
** producer.
|
||||
**
|
||||
** A new instance is not loaded per call: QAlerts loads the sink once per
|
||||
** QInstance and reuses it, so implementations must be thread-safe.
|
||||
*******************************************************************************/
|
||||
public interface AlertSinkInterface
|
||||
{
|
||||
/***************************************************************************
|
||||
** Record (or refresh) an alert. The signal's occurredAt is always set.
|
||||
***************************************************************************/
|
||||
void raise(AlertSignal signal);
|
||||
|
||||
/***************************************************************************
|
||||
** Resolve an open alert, if there is one. resolvedAt is always set.
|
||||
***************************************************************************/
|
||||
void resolve(AlertResolution resolution);
|
||||
|
||||
/***************************************************************************
|
||||
** Note that a dead-man's switch beat.
|
||||
***************************************************************************/
|
||||
void heartbeat(String heartbeatKey, Instant at);
|
||||
|
||||
/***************************************************************************
|
||||
** Send a notification to people (an audience). The default does nothing,
|
||||
** so sinks written before notifications keep working.
|
||||
***************************************************************************/
|
||||
default void notify(NotificationSignal signal)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// noop - sinks that don't carry notifications ignore them //
|
||||
//////////////////////////////////////////////////////////////
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Instance-level alerts meta-data: the sink that QAlerts sends signals to
|
||||
** (default: LoggingAlertSink) and every declared QAlertTypeMetaData. It is
|
||||
** supplemental meta-data, so any module or qbit can add to it.
|
||||
*******************************************************************************/
|
||||
public class AlertsInstanceMetaData implements QSupplementalInstanceMetaData
|
||||
{
|
||||
public static final String NAME = "alertsQBitStandIn";
|
||||
|
||||
/** alert type names: lowercase, dotted, at least two segments. */
|
||||
public static final Pattern ALERT_TYPE_NAME_PATTERN = Pattern.compile("^[a-z0-9]+(\\.[a-z0-9-]+)+$");
|
||||
|
||||
private QCodeReference sink;
|
||||
private Map<String, QAlertTypeMetaData> alertTypes = new LinkedHashMap<>();
|
||||
private List<String> duplicateAlertTypes = new ArrayList<>();
|
||||
|
||||
private transient AlertSinkInterface loadedSink;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the instance's alerts meta-data, or null when nothing added it.
|
||||
*******************************************************************************/
|
||||
public static AlertsInstanceMetaData of(QInstance qInstance)
|
||||
{
|
||||
return (qInstance == null ? null : QSupplementalInstanceMetaData.of(qInstance, NAME));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get the instance's alerts meta-data, adding an empty one if it has none.
|
||||
*******************************************************************************/
|
||||
public static AlertsInstanceMetaData ofOrWithNew(QInstance qInstance)
|
||||
{
|
||||
return (QSupplementalInstanceMetaData.ofOrWithNew(qInstance, NAME, AlertsInstanceMetaData::new));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return (NAME);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sink
|
||||
*******************************************************************************/
|
||||
public QCodeReference getSink()
|
||||
{
|
||||
return (this.sink);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sink - an AlertSinkInterface implementation. An
|
||||
** alerts implementation sets this; producers never do.
|
||||
*******************************************************************************/
|
||||
public AlertsInstanceMetaData withSink(QCodeReference sink)
|
||||
{
|
||||
this.sink = sink;
|
||||
this.loadedSink = null;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The sink instance QAlerts loaded for this meta-data (cached; null until
|
||||
** first use).
|
||||
*******************************************************************************/
|
||||
AlertSinkInterface getLoadedSink()
|
||||
{
|
||||
return (this.loadedSink);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Cache the loaded sink instance.
|
||||
*******************************************************************************/
|
||||
void setLoadedSink(AlertSinkInterface loadedSink)
|
||||
{
|
||||
this.loadedSink = loadedSink;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for alertTypes
|
||||
*******************************************************************************/
|
||||
public Map<String, QAlertTypeMetaData> getAlertTypes()
|
||||
{
|
||||
return (this.alertTypes);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Find a declared type by name, or null.
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData getAlertType(String name)
|
||||
{
|
||||
return (name == null ? null : alertTypes.get(name));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Declare an alert type. Adding the same object again is a no-op; adding a
|
||||
** different type with a name already taken is a validation error.
|
||||
*******************************************************************************/
|
||||
public AlertsInstanceMetaData withAlertType(QAlertTypeMetaData alertType)
|
||||
{
|
||||
if(alertType == null)
|
||||
{
|
||||
return (this);
|
||||
}
|
||||
|
||||
QAlertTypeMetaData existing = alertTypes.get(alertType.getName());
|
||||
if(existing != null && existing != alertType)
|
||||
{
|
||||
duplicateAlertTypes.add(alertType.getName());
|
||||
return (this);
|
||||
}
|
||||
|
||||
alertTypes.put(alertType.getName(), alertType);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Names must be unique and well-formed, subject tables must exist, a
|
||||
** visibility permission must not be blank, heartbeat default monitors need
|
||||
** an interval, and the sink must implement AlertSinkInterface.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void validate(QInstance qInstance, QInstanceValidator validator)
|
||||
{
|
||||
if(sink != null)
|
||||
{
|
||||
validator.validateSimpleCodeReference("Alerts sink: ", sink, AlertSinkInterface.class);
|
||||
}
|
||||
|
||||
for(String duplicate : duplicateAlertTypes)
|
||||
{
|
||||
validator.assertCondition(false, "Alert type " + duplicate + " is declared more than once.");
|
||||
}
|
||||
|
||||
for(Map.Entry<String, QAlertTypeMetaData> entry : alertTypes.entrySet())
|
||||
{
|
||||
String name = entry.getKey();
|
||||
QAlertTypeMetaData alertType = entry.getValue();
|
||||
String prefix = "Alert type " + name + ": ";
|
||||
|
||||
if(!validator.assertCondition(name != null && ALERT_TYPE_NAME_PATTERN.matcher(name).matches(), prefix + "name must be lowercase and dotted (e.g. cdn.purge.dead), matching " + ALERT_TYPE_NAME_PATTERN.pattern()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
validator.assertCondition(alertType.getDefaultSeverity() != null, prefix + "defaultSeverity is required.");
|
||||
|
||||
if(alertType.getSubjectTableName() != null)
|
||||
{
|
||||
validator.assertCondition(qInstance.getTable(alertType.getSubjectTableName()) != null, prefix + "subjectTableName " + alertType.getSubjectTableName() + " is not a table in the instance.");
|
||||
}
|
||||
|
||||
if(alertType.getVisibilityPermission() != null)
|
||||
{
|
||||
validator.assertCondition(!alertType.getVisibilityPermission().isBlank(), prefix + "visibilityPermission must not be blank.");
|
||||
}
|
||||
|
||||
if(alertType.getResolveMode() == ResolveMode.QUIET_PERIOD)
|
||||
{
|
||||
validator.assertCondition(alertType.getQuietPeriod() != null && !alertType.getQuietPeriod().isNegative() && !alertType.getQuietPeriod().isZero(), prefix + "a QUIET_PERIOD resolve mode needs a positive quietPeriod.");
|
||||
}
|
||||
|
||||
for(AlertDefaultMonitor monitor : alertType.getDefaultMonitors())
|
||||
{
|
||||
validator.assertCondition(monitor.getKind() != null, prefix + "a default monitor needs a kind.");
|
||||
if(monitor.getKind() == AlertMonitorKind.HEARTBEAT)
|
||||
{
|
||||
Object every = monitor.getDefinition().get("expectedEverySeconds");
|
||||
validator.assertCondition(every instanceof Number number && number.longValue() > 0, prefix + "a heartbeat default monitor needs a positive expectedEverySeconds.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** How much of an alert an external channel (email, chat, paging, webhook) may
|
||||
** include. A channel has a level; an alert type has a ceiling, and the lower
|
||||
** of the two applies.
|
||||
*******************************************************************************/
|
||||
public enum DetailLevel
|
||||
{
|
||||
/** severity, type label, group, title, age and one link back to the alert. */
|
||||
MINIMAL,
|
||||
|
||||
/** MINIMAL plus the summary and the counts. */
|
||||
STANDARD,
|
||||
|
||||
/** STANDARD plus details and record labels (admin opt-in). */
|
||||
FULL;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The lower (less revealing) of two levels. Null means "no limit".
|
||||
*******************************************************************************/
|
||||
public static DetailLevel lowerOf(DetailLevel a, DetailLevel b)
|
||||
{
|
||||
if(a == null)
|
||||
{
|
||||
return (b);
|
||||
}
|
||||
if(b == null)
|
||||
{
|
||||
return (a);
|
||||
}
|
||||
return (a.compareTo(b) <= 0 ? a : b);
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,16 @@ import java.util.Objects;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** Who a notification is for, resolved to users when it is sent (stand-in for
|
||||
** core's com.kingsrook.qqq.backend.core.alerts.NotificationAudience).
|
||||
*******************************************************************************/
|
||||
public class NotificationAudience implements Serializable
|
||||
{
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** The kinds of audience.
|
||||
*******************************************************************************/
|
||||
public enum Type
|
||||
|
||||
@@ -19,6 +19,8 @@ package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** What a notification is about (stand-in for core's
|
||||
** com.kingsrook.qqq.backend.core.alerts.NotificationCategory). Users filter
|
||||
** and mute by category; SECURITY can never be muted in-app.
|
||||
|
||||
@@ -19,6 +19,8 @@ package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** How a notification looks to its reader: INFO and SUCCESS are good news,
|
||||
** WARNING and ERROR need a look, CRITICAL makes the badge red (stand-in for
|
||||
** core's com.kingsrook.qqq.backend.core.alerts.NotificationSeverity).
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.time.Instant;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** A notification for people (stand-in for core's
|
||||
** com.kingsrook.qqq.backend.core.alerts.NotificationSignal): what to tell
|
||||
** them, how urgent it is, what it is about, and who it is for. Send it with
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.SourceQBitAware;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Declares a kind of problem a qbit or app can report (e.g. cdn.purge.dead),
|
||||
** so the system "knows about" every alert source. An alerts implementation
|
||||
** syncs these into a table at startup, where admins can see and tune them.
|
||||
**
|
||||
** Add it to an instance like any top-level meta-data (qInstance.add(...), or
|
||||
** from a MetaDataProducer); it lands in AlertsInstanceMetaData.alertTypes.
|
||||
** Names are lowercase and dotted: ^[a-z0-9]+(\.[a-z0-9-]+)+$
|
||||
*******************************************************************************/
|
||||
public class QAlertTypeMetaData implements TopLevelMetaDataInterface, SourceQBitAware
|
||||
{
|
||||
private String name;
|
||||
private String label;
|
||||
private String sourceName;
|
||||
private String sourceQBitName;
|
||||
private String groupName;
|
||||
private AlertSeverity defaultSeverity = AlertSeverity.WARNING;
|
||||
private String description;
|
||||
private String howToFix;
|
||||
private String subjectTableName;
|
||||
private String visibilityPermission;
|
||||
private ResolveMode resolveMode = ResolveMode.PRODUCER;
|
||||
private Duration quietPeriod;
|
||||
private List<AlertDefaultMonitor> defaultMonitors = new ArrayList<>();
|
||||
private DetailLevel externalDetailCeiling = DetailLevel.STANDARD;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Find a declared alert type by name, or null.
|
||||
*******************************************************************************/
|
||||
public static QAlertTypeMetaData of(QInstance qInstance, String name)
|
||||
{
|
||||
AlertsInstanceMetaData alertsInstanceMetaData = AlertsInstanceMetaData.of(qInstance);
|
||||
return (alertsInstanceMetaData == null ? null : alertsInstanceMetaData.getAlertType(name));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Adds this type to the instance's AlertsInstanceMetaData (creating it).
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addSelfToInstance(QInstance qInstance)
|
||||
{
|
||||
AlertsInstanceMetaData.ofOrWithNew(qInstance).withAlertType(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The source shown to people: sourceName if set, otherwise the qbit that
|
||||
** produced this meta-data.
|
||||
*******************************************************************************/
|
||||
public String getEffectiveSourceName()
|
||||
{
|
||||
return (sourceName != null ? sourceName : sourceQBitName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for label
|
||||
*******************************************************************************/
|
||||
public String getLabel()
|
||||
{
|
||||
return (this.label);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for label
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withLabel(String label)
|
||||
{
|
||||
this.label = label;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sourceName
|
||||
*******************************************************************************/
|
||||
public String getSourceName()
|
||||
{
|
||||
return (this.sourceName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sourceName
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withSourceName(String sourceName)
|
||||
{
|
||||
this.sourceName = sourceName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for sourceQBitName
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getSourceQBitName()
|
||||
{
|
||||
return (this.sourceQBitName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for sourceQBitName
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setSourceQBitName(String sourceQBitName)
|
||||
{
|
||||
this.sourceQBitName = sourceQBitName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for sourceQBitName
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QAlertTypeMetaData withSourceQBitName(String sourceQBitName)
|
||||
{
|
||||
this.sourceQBitName = sourceQBitName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for groupName
|
||||
*******************************************************************************/
|
||||
public String getGroupName()
|
||||
{
|
||||
return (this.groupName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for groupName (the default group; created if missing)
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withGroupName(String groupName)
|
||||
{
|
||||
this.groupName = groupName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for defaultSeverity
|
||||
*******************************************************************************/
|
||||
public AlertSeverity getDefaultSeverity()
|
||||
{
|
||||
return (this.defaultSeverity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for defaultSeverity
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withDefaultSeverity(AlertSeverity defaultSeverity)
|
||||
{
|
||||
this.defaultSeverity = defaultSeverity;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for description
|
||||
*******************************************************************************/
|
||||
public String getDescription()
|
||||
{
|
||||
return (this.description);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for description
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for howToFix
|
||||
*******************************************************************************/
|
||||
public String getHowToFix()
|
||||
{
|
||||
return (this.howToFix);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for howToFix
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withHowToFix(String howToFix)
|
||||
{
|
||||
this.howToFix = howToFix;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for subjectTableName
|
||||
*******************************************************************************/
|
||||
public String getSubjectTableName()
|
||||
{
|
||||
return (this.subjectTableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for subjectTableName (drives visibility and the record panel)
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withSubjectTableName(String subjectTableName)
|
||||
{
|
||||
this.subjectTableName = subjectTableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for visibilityPermission
|
||||
*******************************************************************************/
|
||||
public String getVisibilityPermission()
|
||||
{
|
||||
return (this.visibilityPermission);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for visibilityPermission (an extra gate on who may see it)
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withVisibilityPermission(String visibilityPermission)
|
||||
{
|
||||
this.visibilityPermission = visibilityPermission;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for resolveMode
|
||||
*******************************************************************************/
|
||||
public ResolveMode getResolveMode()
|
||||
{
|
||||
return (this.resolveMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for resolveMode
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withResolveMode(ResolveMode resolveMode)
|
||||
{
|
||||
this.resolveMode = resolveMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Resolve automatically after this long with no raise (sets the mode to
|
||||
** QUIET_PERIOD).
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withQuietPeriod(Duration quietPeriod)
|
||||
{
|
||||
this.quietPeriod = quietPeriod;
|
||||
this.resolveMode = ResolveMode.QUIET_PERIOD;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for quietPeriod
|
||||
*******************************************************************************/
|
||||
public Duration getQuietPeriod()
|
||||
{
|
||||
return (this.quietPeriod);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for defaultMonitors
|
||||
*******************************************************************************/
|
||||
public List<AlertDefaultMonitor> getDefaultMonitors()
|
||||
{
|
||||
return (this.defaultMonitors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add a seeded monitor for this type.
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withDefaultMonitor(AlertDefaultMonitor defaultMonitor)
|
||||
{
|
||||
if(defaultMonitor != null)
|
||||
{
|
||||
this.defaultMonitors.add(defaultMonitor);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for externalDetailCeiling
|
||||
*******************************************************************************/
|
||||
public DetailLevel getExternalDetailCeiling()
|
||||
{
|
||||
return (this.externalDetailCeiling);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for externalDetailCeiling (the most any channel may include)
|
||||
*******************************************************************************/
|
||||
public QAlertTypeMetaData withExternalDetailCeiling(DetailLevel externalDetailCeiling)
|
||||
{
|
||||
this.externalDetailCeiling = externalDetailCeiling;
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** How an open alert of a type is closed automatically. Any mode also allows
|
||||
** a person to resolve an alert manually, with a note.
|
||||
*******************************************************************************/
|
||||
public enum ResolveMode
|
||||
{
|
||||
/** the producer calls QAlerts.resolve when the condition clears. */
|
||||
PRODUCER,
|
||||
|
||||
/** the monitor that raised it resolves it when its next evaluation is clean. */
|
||||
MONITOR,
|
||||
|
||||
/** it resolves when no raise has been seen for the type's quiet period. */
|
||||
QUIET_PERIOD,
|
||||
|
||||
/** only a person resolves it. */
|
||||
MANUAL
|
||||
}
|
||||
@@ -16,16 +16,20 @@
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** STAND-IN for the QQQ core alerts producer SPI (QRun-IO/qqq#1108), which is
|
||||
** not yet in a released QQQ 4.1.0 RC. These classes are copies of
|
||||
** com.kingsrook.qqq.backend.core.alerts (AlertSignal, AlertSeverity,
|
||||
** QAlertTypeMetaData and friends) under this package, so the qbit can be
|
||||
** built and tested against 4.1.0-RC.4 now.
|
||||
** STAND-IN(core=QRun-IO/qqq#1125, removeBy=4.1.0-RC.6)
|
||||
**
|
||||
** Replace when the RC with qqq#1108 ships: delete this package, change the
|
||||
** imports to com.kingsrook.qqq.backend.core.alerts, and register
|
||||
** QbitAlertsSink in core's AlertsInstanceMetaData (see AlertsQBitProducer).
|
||||
** Until then, producers in a host reach the qbit through
|
||||
** AlertsRuntime.sink() (or its monitors), not through core's QAlerts.
|
||||
** Only NotificationSignal, NotificationAudience, NotificationCategory,
|
||||
** NotificationSeverity and AlertLink remain here. Core 4.1.0-RC.5 ships the
|
||||
** alert SPI, but not the notification API planned in QRun-IO/qqq#1125.
|
||||
**
|
||||
** Released qbit-easypost-tracking 0.1.x calls AlertsRuntime.sink().notify
|
||||
** with these types. AlertLink remains because NotificationSignal's public
|
||||
** signatures use it. Keep their binary signatures until producers migrate
|
||||
** to core QAlerts.notify in RC.6; no local sink interface is needed because
|
||||
** AlertsRuntime.sink() still returns QbitAlertsSink.
|
||||
**
|
||||
** Alerts use com.kingsrook.qqq.backend.core.alerts. AlertsQBitProducer
|
||||
** registers AlertsSinkAdapter through core AlertsInstanceMetaData, so host
|
||||
** QAlerts.raise/resolve/heartbeat calls reach this qbit's runtime.
|
||||
*******************************************************************************/
|
||||
package com.kingsrook.qbits.alerts.spi;
|
||||
@@ -27,8 +27,8 @@ import com.kingsrook.qbits.alerts.metadata.AlertsAppMetaDataProducer;
|
||||
import com.kingsrook.qbits.alerts.metadata.AlertsHomeWidgetsMetaDataProducer;
|
||||
import com.kingsrook.qbits.alerts.metadata.AlertsProcessesMetaDataProducer;
|
||||
import com.kingsrook.qbits.alerts.processes.AlertsAction;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
@@ -28,12 +28,12 @@ import com.kingsrook.qbits.alerts.metadata.AlertsHomeWidgetsMetaDataProducer;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsChannel;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Resolution;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsSubscription;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertRecordRef;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
|
||||
@@ -32,16 +32,16 @@ import com.kingsrook.qbits.alerts.model.AlertsSubscription;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsUserPreference;
|
||||
import com.kingsrook.qbits.alerts.processes.AlertsAction;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertRecordRef;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessSummaryLineInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
|
||||
@@ -25,8 +25,8 @@ import com.kingsrook.qbits.alerts.delivery.AlertMessage;
|
||||
import com.kingsrook.qbits.alerts.delivery.AlertSecretResolver;
|
||||
import com.kingsrook.qbits.alerts.delivery.DeliveryResult;
|
||||
import com.kingsrook.qbits.alerts.engine.AlertCheckInterface;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
|
||||
@@ -26,10 +26,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.kingsrook.qbits.alerts.delivery.channels.ExternalChannels;
|
||||
import com.kingsrook.qbits.alerts.delivery.channels.StandardWebhookSigner;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.webhooks.WebhookSecret;
|
||||
import com.kingsrook.qqq.backend.core.utils.webhooks.WebhookVerifier;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -172,8 +173,8 @@ class ExternalChannelsTest
|
||||
String timestamp = request.headers().get("Webhook-timestamp").get(0);
|
||||
String signature = request.headers().get("Webhook-signature").get(0);
|
||||
assertThat(id).startsWith("msg_");
|
||||
String expected = StandardWebhookSigner.sign(id, Long.parseLong(timestamp), request.body().getBytes(StandardCharsets.UTF_8), "whsec_c2VjcmV0LWtleS1mb3ItdGVzdHM=");
|
||||
assertThat(signature).isEqualTo("v1," + expected);
|
||||
new WebhookVerifier(List.of(WebhookSecret.parse("whsec_c2VjcmV0LWtleS1mb3ItdGVzdHM=")))
|
||||
.verify(Map.of("webhook-id", id, "webhook-timestamp", timestamp, "webhook-signature", signature), request.body().getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(request.body()).contains("\"type\":\"qqq.alert.resolved\"").contains("\"dedupKey\":\"provider:cf\"");
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
@@ -22,10 +22,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.alerts.AlertsQBitConfig;
|
||||
import com.kingsrook.qbits.alerts.engine.AlertRouter;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.ChannelType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.DeliveryKind;
|
||||
import com.kingsrook.qbits.alerts.spi.DetailLevel;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues;
|
||||
import com.kingsrook.qqq.backend.core.alerts.DetailLevel;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.alerts.delivery;
|
||||
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
import com.kingsrook.qbits.alerts.delivery.channels.StandardWebhookSigner;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fixed wire vectors generated independently with Python's hmac/hashlib.
|
||||
** Cover both secret encodings, short legacy keys and a key over one block.
|
||||
*******************************************************************************/
|
||||
class StandardWebhookSignerTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Raw and whsec_ secrets produce the exact same legacy header values.
|
||||
*******************************************************************************/
|
||||
@ParameterizedTest
|
||||
@MethodSource("wireVectors")
|
||||
void testLegacyWireFormat(String key, String expectedSignature)
|
||||
{
|
||||
Instant timestamp = Instant.ofEpochSecond(1791806400L);
|
||||
byte[] body = "{\"event\":\"test\"}".getBytes(StandardCharsets.UTF_8);
|
||||
Map<String, String> expected = Map.of("webhook-id", "msg_test", "webhook-timestamp", "1791806400", "webhook-signature", expectedSignature);
|
||||
assertThat(StandardWebhookSigner.headers("msg_test", timestamp, body, " " + key + " ")).isEqualTo(expected);
|
||||
String encoded = "whsec_" + Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8));
|
||||
assertThat(StandardWebhookSigner.headers("msg_test", timestamp, body, encoded)).isEqualTo(expected);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Independent expected signatures, including the 15/16-byte core boundary.
|
||||
*******************************************************************************/
|
||||
private static Stream<Arguments> wireVectors()
|
||||
{
|
||||
return (Stream.of(
|
||||
Arguments.of("short", "v1,Jz84jUiA/gQwIt0Td1DzUA/OE5soKzC5H9C5e94lliM="),
|
||||
Arguments.of("sixteen-byte-key", "v1,qep/FxanCVlGy2Zohku2Bgd3ufM4KNRcj5qT8t7blNA="),
|
||||
Arguments.of("sixteen-byte-key!", "v1,tbs9jXA3r4N06ZLpuOe6V4RasiRwYwbM2+2ZxRu8lNU="),
|
||||
Arguments.of("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "v1,K6s1XISWGs8vzyUo3Ecush1eoxy/d5CiND+gzS70I8U=")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Empty or malformed keys still fail before any request is sent.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInvalidSecrets()
|
||||
{
|
||||
for(String secret : new String[] { null, "", " ", "whsec_", "whsec_!" })
|
||||
{
|
||||
assertThatThrownBy(() -> StandardWebhookSigner.headers("msg_test", Instant.EPOCH, null, secret)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,19 +40,19 @@ import com.kingsrook.qbits.alerts.model.AlertsSubscription;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsUserPreference;
|
||||
import com.kingsrook.qbits.alerts.notifications.NotificationService;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertDefaultMonitor;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertLink;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertMonitorKind;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertRecordRef;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationAudience;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationCategory;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.NotificationSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertDefaultMonitor;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertMonitorKind;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.ResolveMode;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
|
||||
@@ -309,11 +309,11 @@ public class AlertsDemoServer extends AbstractQQQApplication
|
||||
{
|
||||
runtime.sink().raise(new AlertSignal("backups.job.failed").withDedupKey("nightly-db").withTitle("Nightly database backup failed")
|
||||
.withSummary("The dump step exited with an error.").withDetails("pg_dump: error: connection to server lost")
|
||||
.withRecordRef(AlertRecordRef.of("backup_run", runId)).withLink(AlertLink.toRecord("backup_run", runId, "Open the run")));
|
||||
.withRecordRef(AlertRecordRef.of("backup_run", runId)).withLink(com.kingsrook.qqq.backend.core.alerts.AlertLink.toRecord("backup_run", runId, "Open the run")));
|
||||
}
|
||||
else
|
||||
{
|
||||
runtime.sink().resolve(new com.kingsrook.qbits.alerts.spi.AlertResolution("backups.job.failed", "nightly-db", "The next run succeeded"));
|
||||
runtime.sink().resolve(new com.kingsrook.qqq.backend.core.alerts.AlertResolution("backups.job.failed", "nightly-db", "The next run succeeded"));
|
||||
runtime.sink().heartbeat("backups.nightly-db", clock.instant());
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,7 @@ public class AlertsDemoServer extends AbstractQQQApplication
|
||||
if(day != 27)
|
||||
{
|
||||
clock.set(clock.instant().plus(Duration.ofHours(3)));
|
||||
runtime.sink().resolve(new com.kingsrook.qbits.alerts.spi.AlertResolution("cdn.purge.dead", "provider:cloudfront", "A later purge succeeded"));
|
||||
runtime.sink().resolve(new com.kingsrook.qqq.backend.core.alerts.AlertResolution("cdn.purge.dead", "provider:cloudfront", "A later purge succeeded"));
|
||||
}
|
||||
}
|
||||
if(day == 25)
|
||||
|
||||
@@ -35,13 +35,13 @@ import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Resolution;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.Status;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsRecordRef;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertLink;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertRecordRef;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertResolution;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertLink;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertResolution;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
|
||||
@@ -27,13 +27,13 @@ import com.kingsrook.qbits.alerts.model.AlertsMonitor;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsMute;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsPossibleValues.MonitorStatus;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertDefaultMonitor;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertMonitorKind;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertDefaultMonitor;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertMonitorKind;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
|
||||
@@ -29,19 +29,19 @@ import com.kingsrook.qbits.alerts.delivery.DeliveryWorker;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsAlert;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsChannel;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsDelivery;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsMute;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotification;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotificationRecipient;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsMute;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsSubscription;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsTeam;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsTeamMember;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsType;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsUserPreference;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertRecordRef;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSeverity;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qbits.alerts.spi.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertRecordRef;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
@@ -24,9 +24,13 @@ import com.kingsrook.qbits.alerts.metadata.AlertsProcessesMetaDataProducer;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsAlert;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsHeartbeat;
|
||||
import com.kingsrook.qbits.alerts.processes.AlertsScheduledSteps;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertResolution;
|
||||
import com.kingsrook.qbits.alerts.spi.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertResolution;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSeverity;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertSignal;
|
||||
import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlertTypeMetaData;
|
||||
import com.kingsrook.qqq.backend.core.alerts.QAlerts;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@@ -41,6 +45,38 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
class SinkAndScheduleTest extends BaseAlertsTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** A host using only core reaches the qbit through instance metadata and
|
||||
** the loadable adapter, including the asynchronous queue and persistence.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testCoreProducerReachesInstalledSink() throws Exception
|
||||
{
|
||||
AlertsInstanceMetaData metadata = AlertsInstanceMetaData.of(qInstance);
|
||||
assertThat(metadata.getSink().getName()).isEqualTo(AlertsSinkAdapter.class.getName());
|
||||
assertThat(qInstance.getSupplementalMetaData("alertsQBitStandIn")).isNull();
|
||||
metadata.withAlertType(new QAlertTypeMetaData().withName("host.backup.failed").withDefaultSeverity(AlertSeverity.ERROR));
|
||||
long failures = QAlerts.getProducerFailureCount();
|
||||
|
||||
QAlerts.raise(new AlertSignal("host.backup.failed").withDedupKey("nightly").withTitle("Nightly backup failed"));
|
||||
QAlerts.heartbeat("host.backup.nightly");
|
||||
runtime.sink().stop();
|
||||
|
||||
List<QRecord> alerts = all(AlertsAlert.TABLE_NAME);
|
||||
assertThat(alerts).hasSize(1);
|
||||
assertThat(alerts.get(0).getValueString("title")).isEqualTo("Nightly backup failed");
|
||||
assertThat(alerts.get(0).getValueString("severity")).isEqualTo("ERROR");
|
||||
assertThat(alerts.get(0).getValueString("status")).isEqualTo("OPEN");
|
||||
assertThat(all(AlertsHeartbeat.TABLE_NAME)).hasSize(1);
|
||||
|
||||
QAlerts.resolve("host.backup.failed", "nightly", "Retry succeeded");
|
||||
runtime.sink().stop();
|
||||
assertThat(all(AlertsAlert.TABLE_NAME).get(0).getValueString("status")).isEqualTo("RESOLVED");
|
||||
assertThat(QAlerts.getProducerFailureCount()).isEqualTo(failures);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Process signals on the background queue in these tests.
|
||||
*******************************************************************************/
|
||||
|
||||
@@ -27,9 +27,11 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qbits.alerts.AlertsPreferences;
|
||||
import com.kingsrook.qbits.alerts.AlertsRuntime;
|
||||
import com.kingsrook.qbits.alerts.BaseAlertsTest;
|
||||
import com.kingsrook.qbits.alerts.api.AlertsApi;
|
||||
import com.kingsrook.qbits.alerts.engine.AlertsDb;
|
||||
import com.kingsrook.qbits.alerts.engine.QbitAlertsSink;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotification;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotificationPreference;
|
||||
import com.kingsrook.qbits.alerts.model.AlertsNotificationRecipient;
|
||||
@@ -346,6 +348,36 @@ class NotificationsTest extends BaseAlertsTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Preserve the method descriptors used by released notification producers,
|
||||
** including the local AlertLink, and deliver to the selected user's bell.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testLegacyNotificationSinkSignaturesAndDelivery() throws Exception
|
||||
{
|
||||
assertThat(AlertsRuntime.class.getMethod("sink").getReturnType()).isEqualTo(QbitAlertsSink.class);
|
||||
assertThat(QbitAlertsSink.class.getMethod("notify", NotificationSignal.class).getReturnType()).isEqualTo(void.class);
|
||||
assertThat(NotificationSignal.class.getMethod("withLink", AlertLink.class).getReturnType()).isEqualTo(NotificationSignal.class);
|
||||
assertThat(NotificationSignal.class.getMethod("getLink").getReturnType()).isEqualTo(AlertLink.class);
|
||||
assertThat(NotificationSignal.class.getMethod("withAudience", NotificationAudience.class).getReturnType()).isEqualTo(NotificationSignal.class);
|
||||
assertThat(AlertLink.class.getMethod("toRecord", String.class, Serializable.class, String.class).getReturnType()).isEqualTo(AlertLink.class);
|
||||
assertThat(NotificationAudience.class.getMethod("user", String.class).getReturnType()).isEqualTo(NotificationAudience.class);
|
||||
|
||||
runtime.sink().notify(new NotificationSignal("Shipment delivered")
|
||||
.withSeverity(NotificationSeverity.INFO).withCategory(NotificationCategory.JOB)
|
||||
.withLink(AlertLink.toRecord("widget", 5, "Shipment"))
|
||||
.withAudience(NotificationAudience.user("viewer-1")));
|
||||
|
||||
loginAs("viewer-1");
|
||||
Map<String, Object> summary = new AlertsApi(runtime).inboxSummary();
|
||||
assertThat(summary.get("unreadCount")).isEqualTo(1);
|
||||
assertThat(items(summary).get(0)).containsEntry("title", "Shipment delivered").containsEntry("href", "/app/widget/5");
|
||||
loginAs("viewer-2");
|
||||
assertThat(new AlertsApi(runtime).inboxSummary().get("unreadCount")).isEqualTo(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Audiences: roles from a directory, permission holders, warnings.
|
||||
*******************************************************************************/
|
||||
|
||||
Reference in new issue
Block a user