Merge branch 'feat/webhook-text-wrap-stellix-pattern' into 'main'
feat(webhook): text-wrap payload для chat-bot receivers (stellix pattern) See merge request 2-6/2-6-4/terravault/ordinis!216
This commit is contained in:
+77
-1
@@ -6,6 +6,7 @@ import cloud.nstart.terravault.ordinis.domain.webhook.WebhookDelivery;
|
|||||||
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookDeliveryRepository;
|
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookDeliveryRepository;
|
||||||
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscription;
|
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscription;
|
||||||
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscriptionRepository;
|
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscriptionRepository;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.micrometer.core.instrument.Counter;
|
import io.micrometer.core.instrument.Counter;
|
||||||
import io.micrometer.core.instrument.MeterRegistry;
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
@@ -76,6 +77,17 @@ public class WebhookDispatcher {
|
|||||||
@Value("${ordinis.webhook.allowed-hosts:}")
|
@Value("${ordinis.webhook.allowed-hosts:}")
|
||||||
private String allowedHostsCsv;
|
private String allowedHostsCsv;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hosts что ожидают text-shaped payload {"text": "..."} вместо raw event JSON
|
||||||
|
* (пример: chat.nstart.cloud Webhook Bot, Mattermost-style incoming hooks).
|
||||||
|
* CSV. Default empty — все subscriptions получают raw JSON. Для chat-bot
|
||||||
|
* URLs выставить hosts через ORDINIS_WEBHOOK_TEXT_WRAP_HOSTS env. Pattern
|
||||||
|
* учится у stellix alert-bridge: receivers что показывают alerts человеку
|
||||||
|
* требуют human-readable text, не структурированный JSON.
|
||||||
|
*/
|
||||||
|
@Value("${ordinis.webhook.text-wrap-hosts:}")
|
||||||
|
private String textWrapHostsCsv;
|
||||||
|
|
||||||
public WebhookDispatcher(
|
public WebhookDispatcher(
|
||||||
OutboxEventRepository outboxRepository,
|
OutboxEventRepository outboxRepository,
|
||||||
WebhookSubscriptionRepository subscriptionRepository,
|
WebhookSubscriptionRepository subscriptionRepository,
|
||||||
@@ -219,7 +231,26 @@ public class WebhookDispatcher {
|
|||||||
|
|
||||||
Timer.Sample sample = Timer.start();
|
Timer.Sample sample = Timer.start();
|
||||||
try {
|
try {
|
||||||
byte[] body = objectMapper.writeValueAsBytes(event.getPayload());
|
// Text-wrap для chat-bot receivers (Mattermost @WebhookBot и подобные).
|
||||||
|
// Stellix pattern: alert получатель видит human-readable string, не raw
|
||||||
|
// JSON. Wrap'аем как {"text": "<rendered>"} если URL host в allowlist.
|
||||||
|
// Без wrap чат-бот получает structured JSON, не показывает в чат
|
||||||
|
// ничего интересного OR вообще rejects.
|
||||||
|
byte[] body;
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(sub.getUrl());
|
||||||
|
String host = uri.getHost() != null ? uri.getHost().toLowerCase() : "";
|
||||||
|
if (shouldTextWrap(host)) {
|
||||||
|
String rendered = renderEventAsText(event);
|
||||||
|
var wrapped = java.util.Map.of("text", rendered);
|
||||||
|
body = objectMapper.writeValueAsBytes(wrapped);
|
||||||
|
} else {
|
||||||
|
body = objectMapper.writeValueAsBytes(event.getPayload());
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException uriEx) {
|
||||||
|
// Bad URL — fallback to raw, dispatcher logged elsewhere.
|
||||||
|
body = objectMapper.writeValueAsBytes(event.getPayload());
|
||||||
|
}
|
||||||
String signature = HmacSigner.sign(sub.getHmacSecret(), body);
|
String signature = HmacSigner.sign(sub.getHmacSecret(), body);
|
||||||
|
|
||||||
HttpRequest req = HttpRequest.newBuilder()
|
HttpRequest req = HttpRequest.newBuilder()
|
||||||
@@ -271,4 +302,49 @@ public class WebhookDispatcher {
|
|||||||
}
|
}
|
||||||
deliveryRepository.save(delivery);
|
deliveryRepository.save(delivery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true если URL host матчит {@code ordinis.webhook.text-wrap-hosts}
|
||||||
|
* CSV — payload завернётся в {@code {"text": "..."}} как ожидают
|
||||||
|
* Mattermost-style chat bots. См. stellix/monitoring/alert-bridge
|
||||||
|
* для prior art.
|
||||||
|
*/
|
||||||
|
private boolean shouldTextWrap(String host) {
|
||||||
|
if (host == null || host.isBlank() || textWrapHostsCsv == null
|
||||||
|
|| textWrapHostsCsv.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String h : textWrapHostsCsv.split(",")) {
|
||||||
|
if (host.equalsIgnoreCase(h.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render OutboxEvent в human-readable single-line/multi-line text для
|
||||||
|
* chat-bot delivery. Не задействует i18n templates (которые в
|
||||||
|
* ordinis-notifications) — webhook bridge должен быть self-contained
|
||||||
|
* на случай если notifications module disabled.
|
||||||
|
*
|
||||||
|
* <p>Format: {@code [<eventType>] <дictionary>/<businessKey> by <userId>}
|
||||||
|
* + payload preview if present. Mirrors stellix renderAlert() shape.
|
||||||
|
*/
|
||||||
|
private String renderEventAsText(OutboxEvent event) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("[").append(event.getEventType()).append("]");
|
||||||
|
JsonNode p = event.getPayload();
|
||||||
|
if (p != null && !p.isMissingNode()) {
|
||||||
|
JsonNode dict = p.get("dictionary");
|
||||||
|
JsonNode bk = p.get("businessKey");
|
||||||
|
JsonNode op = p.get("operation");
|
||||||
|
JsonNode maker = p.get("maker");
|
||||||
|
if (dict != null) sb.append(" ").append(dict.asText());
|
||||||
|
if (bk != null) sb.append("/").append(bk.asText());
|
||||||
|
if (op != null) sb.append(" op=").append(op.asText());
|
||||||
|
if (maker != null) sb.append(" by=").append(maker.asText());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user