diff --git a/AGENTS.md b/AGENTS.md index d229fa8..27d622e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 83ada6c..2b8ca11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 0a1cfe7..ed898d3 100644 --- a/README.md +++ b/README.md @@ -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,`). 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,`). 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. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 6086055..b46a540 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -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). diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md index 26df034..a739ceb 100644 --- a/docs/NOTIFICATIONS.md +++ b/docs/NOTIFICATIONS.md @@ -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 diff --git a/pom.xml b/pom.xml index 78c91d7..881be06 100644 --- a/pom.xml +++ b/pom.xml @@ -158,14 +158,14 @@ - + qqq-snapshot true - 4.1.0-RC.4 + 4.1.0-RC.5 diff --git a/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java b/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java index ebd6015..bebe252 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitConfig.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitProducer.java b/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitProducer.java index fc45f4b..2e43e61 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitProducer.java +++ b/src/main/java/com/kingsrook/qbits/alerts/AlertsQBitProducer.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java b/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java index 4709a90..42f2d97 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java +++ b/src/main/java/com/kingsrook/qbits/alerts/AlertsRuntime.java @@ -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() { diff --git a/src/main/java/com/kingsrook/qbits/alerts/api/AlertsApi.java b/src/main/java/com/kingsrook/qbits/alerts/api/AlertsApi.java index 9873dd0..2c6be5c 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/api/AlertsApi.java +++ b/src/main/java/com/kingsrook/qbits/alerts/api/AlertsApi.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/delivery/AlertMessageRenderer.java b/src/main/java/com/kingsrook/qbits/alerts/delivery/AlertMessageRenderer.java index dfe31c1..2559cb8 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/delivery/AlertMessageRenderer.java +++ b/src/main/java/com/kingsrook/qbits/alerts/delivery/AlertMessageRenderer.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/delivery/DeliveryWorker.java b/src/main/java/com/kingsrook/qbits/alerts/delivery/DeliveryWorker.java index 94385f4..94307e2 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/delivery/DeliveryWorker.java +++ b/src/main/java/com/kingsrook/qbits/alerts/delivery/DeliveryWorker.java @@ -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))); } } diff --git a/src/main/java/com/kingsrook/qbits/alerts/delivery/channels/StandardWebhookSigner.java b/src/main/java/com/kingsrook/qbits/alerts/delivery/channels/StandardWebhookSigner.java index 9be6e1b..cb1c0af 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/delivery/channels/StandardWebhookSigner.java +++ b/src/main/java/com/kingsrook/qbits/alerts/delivery/channels/StandardWebhookSigner.java @@ -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 headers(String messageId, Instant timestamp, byte[] body, String secret) { - long seconds = timestamp.getEpochSecond(); - Map 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)); } } diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertCheckInterface.java b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertCheckInterface.java index f7fd706..b4ca187 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertCheckInterface.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertCheckInterface.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertRouter.java b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertRouter.java index 56b3bb1..76fcd8b 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertRouter.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertRouter.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertTypeSync.java b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertTypeSync.java index 1d032ca..017f28b 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertTypeSync.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertTypeSync.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertmanagerIngest.java b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertmanagerIngest.java index 52daf9e..2eec024 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertmanagerIngest.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertmanagerIngest.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsEngine.java b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsEngine.java index 337d10d..b3d6104 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsEngine.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsEngine.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsProcessListener.java b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsProcessListener.java index d79ef27..1972854 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsProcessListener.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsProcessListener.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsSinkAdapter.java b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsSinkAdapter.java index 668ad44..31044ee 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsSinkAdapter.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/AlertsSinkAdapter.java @@ -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); - } - } } diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluator.java b/src/main/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluator.java index f8d30e6..7104eb6 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluator.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluator.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/engine/QbitAlertsSink.java b/src/main/java/com/kingsrook/qbits/alerts/engine/QbitAlertsSink.java index c932ebf..a631de2 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/engine/QbitAlertsSink.java +++ b/src/main/java/com/kingsrook/qbits/alerts/engine/QbitAlertsSink.java @@ -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) diff --git a/src/main/java/com/kingsrook/qbits/alerts/home/AlertSummaryLoader.java b/src/main/java/com/kingsrook/qbits/alerts/home/AlertSummaryLoader.java index 1a6ac28..6978c5c 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/home/AlertSummaryLoader.java +++ b/src/main/java/com/kingsrook/qbits/alerts/home/AlertSummaryLoader.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/home/AlertsHome.java b/src/main/java/com/kingsrook/qbits/alerts/home/AlertsHome.java index 6452d2f..dcf0646 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/home/AlertsHome.java +++ b/src/main/java/com/kingsrook/qbits/alerts/home/AlertsHome.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/model/AlertsPossibleValues.java b/src/main/java/com/kingsrook/qbits/alerts/model/AlertsPossibleValues.java index f4600cb..747105b 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/model/AlertsPossibleValues.java +++ b/src/main/java/com/kingsrook/qbits/alerts/model/AlertsPossibleValues.java @@ -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; diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertDefaultMonitor.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertDefaultMonitor.java deleted file mode 100644 index 0f5f524..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertDefaultMonitor.java +++ /dev/null @@ -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 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 getDefinition() - { - return (this.definition); - } - - - - /******************************************************************************* - ** Fluent setter for definition - *******************************************************************************/ - public AlertDefaultMonitor withDefinition(Map 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); - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertLink.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertLink.java index b6e24a5..afcbe58 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertLink.java +++ b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertLink.java @@ -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. diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertMonitorKind.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertMonitorKind.java deleted file mode 100644 index e340927..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertMonitorKind.java +++ /dev/null @@ -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 -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertRecordRef.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertRecordRef.java deleted file mode 100644 index 45009ad..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertRecordRef.java +++ /dev/null @@ -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 of(String tableName, Collection primaryKeys) - { - List 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); - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertResolution.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertResolution.java deleted file mode 100644 index 33a8232..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertResolution.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSeverity.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSeverity.java deleted file mode 100644 index 1069ee3..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSeverity.java +++ /dev/null @@ -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; - }; - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSignal.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSignal.java deleted file mode 100644 index 24599c0..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSignal.java +++ /dev/null @@ -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 recordRefs = new ArrayList<>(); - private List links = new ArrayList<>(); - private Map 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 getRecordRefs() - { - return (this.recordRefs); - } - - - - /******************************************************************************* - ** Setter for recordRefs - *******************************************************************************/ - public void setRecordRefs(List 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 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 getLinks() - { - return (this.links); - } - - - - /******************************************************************************* - ** Setter for links - *******************************************************************************/ - public void setLinks(List 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 getAttributes() - { - return (this.attributes); - } - - - - /******************************************************************************* - ** Setter for attributes - *******************************************************************************/ - public void setAttributes(Map 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); - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSinkInterface.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSinkInterface.java deleted file mode 100644 index 4a87e6f..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertSinkInterface.java +++ /dev/null @@ -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 // - ////////////////////////////////////////////////////////////// - } - -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertsInstanceMetaData.java b/src/main/java/com/kingsrook/qbits/alerts/spi/AlertsInstanceMetaData.java deleted file mode 100644 index 58e193d..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/AlertsInstanceMetaData.java +++ /dev/null @@ -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 alertTypes = new LinkedHashMap<>(); - private List 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 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 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."); - } - } - } - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/DetailLevel.java b/src/main/java/com/kingsrook/qbits/alerts/spi/DetailLevel.java deleted file mode 100644 index bcea373..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/DetailLevel.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationAudience.java b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationAudience.java index 783b848..6a1dab6 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationAudience.java +++ b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationAudience.java @@ -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 diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationCategory.java b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationCategory.java index b5565a6..6cb8f68 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationCategory.java +++ b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationCategory.java @@ -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. diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSeverity.java b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSeverity.java index 4bf897a..801832f 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSeverity.java +++ b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSeverity.java @@ -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). diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSignal.java b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSignal.java index a38070e..37517cf 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSignal.java +++ b/src/main/java/com/kingsrook/qbits/alerts/spi/NotificationSignal.java @@ -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 diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/QAlertTypeMetaData.java b/src/main/java/com/kingsrook/qbits/alerts/spi/QAlertTypeMetaData.java deleted file mode 100644 index 914cbbd..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/QAlertTypeMetaData.java +++ /dev/null @@ -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 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 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); - } -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/ResolveMode.java b/src/main/java/com/kingsrook/qbits/alerts/spi/ResolveMode.java deleted file mode 100644 index 37b724e..0000000 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/ResolveMode.java +++ /dev/null @@ -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 -} diff --git a/src/main/java/com/kingsrook/qbits/alerts/spi/package-info.java b/src/main/java/com/kingsrook/qbits/alerts/spi/package-info.java index f8c6e78..ee16217 100644 --- a/src/main/java/com/kingsrook/qbits/alerts/spi/package-info.java +++ b/src/main/java/com/kingsrook/qbits/alerts/spi/package-info.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/AlertsQBitProducerTest.java b/src/test/java/com/kingsrook/qbits/alerts/AlertsQBitProducerTest.java index 7a18230..eea5685 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/AlertsQBitProducerTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/AlertsQBitProducerTest.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/HomeAndApiTest.java b/src/test/java/com/kingsrook/qbits/alerts/HomeAndApiTest.java index 3a162d1..809f374 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/HomeAndApiTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/HomeAndApiTest.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/SecurityAndActionsTest.java b/src/test/java/com/kingsrook/qbits/alerts/SecurityAndActionsTest.java index 80f56dc..19fb91d 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/SecurityAndActionsTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/SecurityAndActionsTest.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/TestSteps.java b/src/test/java/com/kingsrook/qbits/alerts/TestSteps.java index bdcf786..fa50169 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/TestSteps.java +++ b/src/test/java/com/kingsrook/qbits/alerts/TestSteps.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/delivery/ExternalChannelsTest.java b/src/test/java/com/kingsrook/qbits/alerts/delivery/ExternalChannelsTest.java index 0a40b55..19db70b 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/delivery/ExternalChannelsTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/delivery/ExternalChannelsTest.java @@ -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\""); //////////////////////////////////////////////////// diff --git a/src/test/java/com/kingsrook/qbits/alerts/delivery/RenderingTest.java b/src/test/java/com/kingsrook/qbits/alerts/delivery/RenderingTest.java index 6e22dc3..cdd7e7c 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/delivery/RenderingTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/delivery/RenderingTest.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/delivery/StandardWebhookSignerTest.java b/src/test/java/com/kingsrook/qbits/alerts/delivery/StandardWebhookSignerTest.java new file mode 100644 index 0000000..3c4056c --- /dev/null +++ b/src/test/java/com/kingsrook/qbits/alerts/delivery/StandardWebhookSignerTest.java @@ -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 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 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); + } + } +} diff --git a/src/test/java/com/kingsrook/qbits/alerts/demo/AlertsDemoServer.java b/src/test/java/com/kingsrook/qbits/alerts/demo/AlertsDemoServer.java index 33e9fde..7c17eee 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/demo/AlertsDemoServer.java +++ b/src/test/java/com/kingsrook/qbits/alerts/demo/AlertsDemoServer.java @@ -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) diff --git a/src/test/java/com/kingsrook/qbits/alerts/engine/AlertsEngineTest.java b/src/test/java/com/kingsrook/qbits/alerts/engine/AlertsEngineTest.java index 604ecc2..92ae668 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/engine/AlertsEngineTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/engine/AlertsEngineTest.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluatorTest.java b/src/test/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluatorTest.java index d673ea8..ac35723 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluatorTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/engine/MonitorEvaluatorTest.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/engine/RoutingAndDeliveryTest.java b/src/test/java/com/kingsrook/qbits/alerts/engine/RoutingAndDeliveryTest.java index d7b78da..05b36d3 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/engine/RoutingAndDeliveryTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/engine/RoutingAndDeliveryTest.java @@ -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; diff --git a/src/test/java/com/kingsrook/qbits/alerts/engine/SinkAndScheduleTest.java b/src/test/java/com/kingsrook/qbits/alerts/engine/SinkAndScheduleTest.java index fe4ff63..5de432f 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/engine/SinkAndScheduleTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/engine/SinkAndScheduleTest.java @@ -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 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. *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qbits/alerts/notifications/NotificationsTest.java b/src/test/java/com/kingsrook/qbits/alerts/notifications/NotificationsTest.java index 108c214..5df58f6 100644 --- a/src/test/java/com/kingsrook/qbits/alerts/notifications/NotificationsTest.java +++ b/src/test/java/com/kingsrook/qbits/alerts/notifications/NotificationsTest.java @@ -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 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. *******************************************************************************/