@kwirthmagnify/kwirth-sender-tee 0.1.12 → 0.1.14
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.
- package/README.md +79 -0
- package/front.js +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
## Cuándo usarlo
|
|
6
|
+
|
|
7
|
+
- Enviar la misma alerta simultáneamente a consola y a fichero.
|
|
8
|
+
- Construir pipelines de notificación: un único punto de entrada que abanica hacia varios destinos.
|
|
9
|
+
- Encadenar tees para topologías más complejas (un tee apuntando a otro tee).
|
|
10
|
+
|
|
11
|
+
## Configuración
|
|
12
|
+
|
|
13
|
+
Cada config tiene un campo `targets`: lista de pares `(senderId, configName)` que identifican el sender y la config de destino.
|
|
14
|
+
|
|
15
|
+
| Campo | Tipo | Requerido | Descripción |
|
|
16
|
+
|-----------|-----------------------------------|-----------|------------------------------------------------|
|
|
17
|
+
| `name` | `string` | Sí | Nombre de esta config |
|
|
18
|
+
| `targets` | `Array<{senderId, configName}>` | Sí | Lista de senders de destino |
|
|
19
|
+
|
|
20
|
+
### Estructura de un target
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"senderId": "console", // id del sender destino (ej: "console", "file", "tee")
|
|
25
|
+
"configName": "mi-console" // nombre de la config ya registrada en ese sender
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Ejemplos
|
|
30
|
+
|
|
31
|
+
### Fan-out a consola y fichero
|
|
32
|
+
|
|
33
|
+
Prerequisito: tener configurados un `ConsoleSender` con config `"alerts-console"` y un `FileSender` con config `"alerts-file"`.
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"name": "alerts-tee",
|
|
38
|
+
"targets": [
|
|
39
|
+
{ "senderId": "console", "configName": "alerts-console" },
|
|
40
|
+
{ "senderId": "file", "configName": "alerts-file" }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Invocar:
|
|
46
|
+
```ts
|
|
47
|
+
senderAccess.send('tee', 'alerts-tee', {
|
|
48
|
+
level: 'error',
|
|
49
|
+
subject: 'Pod crash',
|
|
50
|
+
body: 'nginx-7d4f crashed in namespace prod'
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
El mensaje llega al mismo tiempo a stdout y al fichero de log.
|
|
55
|
+
|
|
56
|
+
### Cadena de tees
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
tee-main
|
|
60
|
+
├─ tee-ops → console + file
|
|
61
|
+
└─ tee-biz → kafka + slack
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Config `tee-main`:
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"name": "tee-main",
|
|
68
|
+
"targets": [
|
|
69
|
+
{ "senderId": "tee", "configName": "tee-ops" },
|
|
70
|
+
{ "senderId": "tee", "configName": "tee-biz" }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Comportamiento
|
|
76
|
+
|
|
77
|
+
- Los envíos a todos los targets se hacen en **paralelo** (`Promise.all`).
|
|
78
|
+
- Si un target falla, el error se propaga (el `Promise.all` rechaza). Los targets completados antes del fallo **no se revierten**.
|
|
79
|
+
- 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, {
|
|
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.
|
|
7
|
+
"version": "0.1.14",
|
|
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": []
|