@kwirthmagnify/kwirth-sender-tee 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +92 -0
  2. package/front.js +1 -1
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Tee Sender
2
+
3
+ Sender intermedio que recibe un mensaje y lo reenvía en paralelo a múltiples senders de destino configurados, sin hacer ningún envío final él mismo. Funciona como el comando Unix `tee`: lo que entra, sale por todos los canales a la vez.
4
+
5
+ Part of [Kwirth](https://kwirthmagnify.dev) — real-time Kubernetes observability
6
+ ([source](https://github.com/kwirthmagnify/kwirth)).
7
+
8
+ ## Install
9
+
10
+ From the Kwirth extension manager, or directly:
11
+
12
+ ```
13
+ https://registry.npmjs.org/@kwirthmagnify/kwirth-sender-tee/-/kwirth-sender-tee-<version>.tgz
14
+ ```
15
+
16
+ It needs no core restart.
17
+
18
+ ## Cuándo usarlo
19
+
20
+ - Enviar la misma alerta simultáneamente a consola y a fichero.
21
+ - Construir pipelines de notificación: un único punto de entrada que abanica hacia varios destinos.
22
+ - Encadenar tees para topologías más complejas (un tee apuntando a otro tee).
23
+
24
+ ## Configuración
25
+
26
+ Cada config tiene un campo `targets`: lista de pares `(senderId, configName)` que identifican el sender y la config de destino.
27
+
28
+ | Campo | Tipo | Requerido | Descripción |
29
+ |-----------|-----------------------------------|-----------|------------------------------------------------|
30
+ | `name` | `string` | Sí | Nombre de esta config |
31
+ | `targets` | `Array<{senderId, configName}>` | Sí | Lista de senders de destino |
32
+
33
+ ### Estructura de un target
34
+
35
+ ```json
36
+ {
37
+ "senderId": "console", // id del sender destino (ej: "console", "file", "tee")
38
+ "configName": "mi-console" // nombre de la config ya registrada en ese sender
39
+ }
40
+ ```
41
+
42
+ ## Ejemplos
43
+
44
+ ### Fan-out a consola y fichero
45
+
46
+ Prerequisito: tener configurados un `ConsoleSender` con config `"alerts-console"` y un `FileSender` con config `"alerts-file"`.
47
+
48
+ ```json
49
+ {
50
+ "name": "alerts-tee",
51
+ "targets": [
52
+ { "senderId": "console", "configName": "alerts-console" },
53
+ { "senderId": "file", "configName": "alerts-file" }
54
+ ]
55
+ }
56
+ ```
57
+
58
+ Invocar:
59
+ ```ts
60
+ senderAccess.send('tee', 'alerts-tee', {
61
+ level: 'error',
62
+ subject: 'Pod crash',
63
+ body: 'nginx-7d4f crashed in namespace prod'
64
+ })
65
+ ```
66
+
67
+ El mensaje llega al mismo tiempo a stdout y al fichero de log.
68
+
69
+ ### Cadena de tees
70
+
71
+ ```
72
+ tee-main
73
+ ├─ tee-ops → console + file
74
+ └─ tee-biz → kafka + slack
75
+ ```
76
+
77
+ Config `tee-main`:
78
+ ```json
79
+ {
80
+ "name": "tee-main",
81
+ "targets": [
82
+ { "senderId": "tee", "configName": "tee-ops" },
83
+ { "senderId": "tee", "configName": "tee-biz" }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ ## Comportamiento
89
+
90
+ - Los envíos a todos los targets se hacen en **paralelo** (`Promise.all`).
91
+ - Si un target falla, el error se propaga (el `Promise.all` rechaza). Los targets completados antes del fallo **no se revierten**.
92
+ - El `ISenderAccess` se inyecta en `startSender()` — el tee no funciona antes de que el sistema lo inicialice.
package/front.js CHANGED
@@ -199,7 +199,7 @@
199
199
  ), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, "Targets"), /* @__PURE__ */ import_react.default.createElement(import_material.Box, { sx: { flex: 1, border: 1, borderColor: "divider", borderRadius: 1, overflowY: "auto" } }, targets.length === 0 ? /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "text.disabled", sx: { p: 1, display: "block" } }, "No targets yet.") : targets.map((t, i) => /* @__PURE__ */ import_react.default.createElement(import_material.Box, { key: i, sx: { display: "flex", alignItems: "center", gap: 1, px: 1, py: 0.5, borderBottom: 1, borderColor: "divider" } }, /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", sx: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, t.senderId), /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "body2", color: "text.secondary", sx: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, t.configName), /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "error", onClick: () => setTargets((prev) => prev.filter((_, idx) => idx !== i)) }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Delete, { sx: { fontSize: 14 } }))))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", spacing: 1, alignItems: "center" }, /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 1 } }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Sender"), /* @__PURE__ */ import_react.default.createElement(import_material.Select, { label: "Sender", value: newSenderId, onChange: (e) => {
200
200
  setNewSenderId(e.target.value);
201
201
  setNewConfigName("");
202
- } }, senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName || s.id)))), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 1 }, disabled: !newSenderId }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Config"), /* @__PURE__ */ import_react.default.createElement(import_material.Select, { label: "Config", value: newConfigName, onChange: (e) => setNewConfigName(e.target.value) }, newSenderConfigs.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c)))), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Add target" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "primary", disabled: !newSenderId || !newConfigName, onClick: addTarget }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null))))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", justifyContent: "flex-end", alignItems: "center", spacing: 1 }, error && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "error", sx: { flex: 1 } }, error), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", variant: "contained", disabled: saving || !editName.trim(), onClick: save }, saving ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : selectedName ? "Update" : "Add")))), /* @__PURE__ */ import_react.default.createElement(import_material.DialogActions, null, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", onClick: onClose }, "Close")));
202
+ } }, senders.map((s) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: s.id, value: s.id }, s.displayName || s.id)))), /* @__PURE__ */ import_react.default.createElement(import_material.FormControl, { size: "small", sx: { flex: 1 }, disabled: !newSenderId }, /* @__PURE__ */ import_react.default.createElement(import_material.InputLabel, null, "Config"), /* @__PURE__ */ import_react.default.createElement(import_material.Select, { label: "Config", value: newConfigName, onChange: (e) => setNewConfigName(e.target.value) }, newSenderConfigs.map((c) => /* @__PURE__ */ import_react.default.createElement(import_material.MenuItem, { key: c, value: c }, c)))), /* @__PURE__ */ import_react.default.createElement(import_material.Tooltip, { title: "Add target" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement(import_material.IconButton, { size: "small", color: "primary", disabled: !newSenderId || !newConfigName, onClick: addTarget }, /* @__PURE__ */ import_react.default.createElement(import_icons_material.Add, null))))), /* @__PURE__ */ import_react.default.createElement(import_material.Stack, { direction: "row", justifyContent: "flex-end", alignItems: "center", spacing: 1 }, error && /* @__PURE__ */ import_react.default.createElement(import_material.Typography, { variant: "caption", color: "error", sx: { flex: 1 } }, error), /* @__PURE__ */ import_react.default.createElement(import_material.Button, { size: "small", variant: "contained", disabled: saving || !editName.trim(), onClick: save }, saving ? /* @__PURE__ */ import_react.default.createElement(import_material.CircularProgress, { size: 14 }) : selectedName ? "Update" : "Add")))), /* @__PURE__ */ import_react.default.createElement(import_material.DialogActions, null, /* @__PURE__ */ import_react.default.createElement(import_material.Button, { onClick: onClose }, "Close")));
203
203
  };
204
204
  var TeeSenderDialog_default = TeeSenderDialog;
205
205
 
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "id": "tee",
5
5
  "name": "@kwirthmagnify/kwirth-sender-tee",
6
6
  "displayName": "Tee Sender",
7
- "version": "0.1.13",
7
+ "version": "0.1.15",
8
8
  "description": "Tee sender plugin for Kwirth — fans out an incoming message to multiple downstream senders simultaneously, acting as an intermediate pipeline node",
9
9
  "requiresRestart": false,
10
10
  "requiresExtension": []