datapipeline 3.2.0: Benachrichtigungen mit Webhook, Slack und E-Mail
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# DataPipeline – ETL-Framework in Ruby
|
||||
|
||||
Stand: 2026-10-09 · Gem `datapipeline` 3.1.0 ([CHANGELOG.md](CHANGELOG.md)) · Ruby 3.4 · Lizenz BSD-3-Clause ([LICENSE](LICENSE))
|
||||
Stand: 2026-10-09 · Gem `datapipeline` 3.2.0 ([CHANGELOG.md](CHANGELOG.md)) · Ruby 3.4 · Lizenz BSD-3-Clause ([LICENSE](LICENSE))
|
||||
|
||||
`DataPipeline` liest Daten aus einer Quelle, schickt jede Zeile durch eine Kette von Schritten (prüfen, filtern,
|
||||
umbauen, anreichern, abgleichen) und schreibt das Ergebnis in ein oder mehrere Ziele. Eine Pipeline ist ein kurzes
|
||||
@@ -14,7 +14,7 @@ Festbreiten-Dateien und REST-APIs.
|
||||
3. [Pipelines schreiben](#3-pipelines-schreiben) – DSL, Schritte, Prüfen, Fehler, Ergebnis
|
||||
4. [Plugins](#4-plugins) – mitgelieferte Quellen und Ziele, eigene Plugins, Abhängigkeiten
|
||||
5. [Satzarten-Layouts](#5-satzarten-layouts) – Dateien mit Kopf-, Positions- und Schlusssätzen
|
||||
6. [Betrieb](#6-betrieb) – Kommandozeile, Verbindungen, Sperre, Abgleich, Historie, Benachrichtigung, Fehlersuche
|
||||
6. [Betrieb](#6-betrieb) – Kommandozeile, Verbindungen, Sperre, Abgleich, Historie, Benachrichtigungen, Fehlersuche
|
||||
7. [Umstieg](#7-umstieg) – auf 3.1, von 2.x auf 3.0 (`transform` entfernt), von 2.0 auf 2.1
|
||||
8. [Entwicklung](#8-entwicklung) – Aufbau, Tests, Gem bauen
|
||||
9. [Ausblick](#9-ausblick)
|
||||
@@ -32,7 +32,8 @@ Festbreiten-Dateien und REST-APIs.
|
||||
| Prüfen | Regeln je Spalte (Pflicht, Länge, Format, Werteliste, Typ), eigene Regeln als Block, Satzarten-Layouts; als Fehler oder nur als Warnung |
|
||||
| Fehler | Abbruch, Zeile überspringen oder mit Grund in eine Fehlerdatei schreiben |
|
||||
| Datenmengen | Batch im Speicher oder Streaming Zeile für Zeile mit konstantem Speicherbedarf; Postgres-Cursor und Schreiben in Paketen |
|
||||
| Betrieb | Kommandozeile `etl` mit Exit-Codes, Dry-Run, Limit, inkrementelle Läufe (Wasserzeichen), Sperre gegen parallele Läufe, Upsert für wiederholbare Läufe, Summenabgleich vor dem Schreiben, Laufhistorie, Verbindungsdatei, Slack |
|
||||
| Betrieb | Kommandozeile `etl` mit Exit-Codes, Dry-Run, Limit, inkrementelle Läufe (Wasserzeichen), Sperre gegen parallele Läufe, Upsert für wiederholbare Läufe, Summenabgleich vor dem Schreiben, Laufhistorie, Verbindungsdatei |
|
||||
| Benachrichtigung | Berichte an Webhooks (signiert), Slack und E-Mail, je Pipeline oder je Skript; eigene Notifier |
|
||||
| Dateiformate | Zeichensatz (z. B. Windows-1252) für CSV und Festbreite, BOM für CSV; Satzarten-Dateien schreiben, prüfen und lesen |
|
||||
|
||||
**Anwendungen:** Das Projekt `etl` nutzt das Gem für die PDS → SAP-Schnittstelle (CUDO) und für das Anlegen von
|
||||
@@ -106,7 +107,7 @@ der Code hier genau der Datei entspricht.
|
||||
| [2.3 Inkrementell und gestreamt](#23-inkrementell-und-gestreamt) | Wasserzeichen, Streaming |
|
||||
| [2.4 Eigenes Plugin](#24-eigenes-plugin) | eine Quelle selbst schreiben |
|
||||
| [2.5 REST-APIs](#25-rest-apis) | Paging, Wiederholung, Pakete, eine URL je Datensatz |
|
||||
| [2.6 Produktiver Lauf](#26-produktiver-lauf) | Verbindungsdatei, Sperre, Kurzschritte, Summenabgleich, Upsert, Zeichensatz, Historie, Slack |
|
||||
| [2.6 Produktiver Lauf](#26-produktiver-lauf) | Verbindungsdatei, Sperre, Kurzschritte, Summenabgleich, Upsert, Zeichensatz, Historie, Benachrichtigung |
|
||||
|
||||
### 2.1 Kundenimport
|
||||
|
||||
@@ -624,13 +625,11 @@ FileUtils.mkdir_p(out)
|
||||
# Zugangsdaten stehen nicht im Skript, sondern in der Verbindungsdatei (Default: config/connections.yml)
|
||||
DataPipeline.connections_file = File.expand_path('config/connections.yml', __dir__)
|
||||
|
||||
# Slack nur, wenn ein Token gesetzt ist; so fällt ein fehlender Token schon vor dem Lauf auf
|
||||
slack = DataPipeline::Slack.new(channel: '#etl') if ENV['SLACK_TOKEN']
|
||||
|
||||
DataPipeline.new do
|
||||
name 'buchungen_export'
|
||||
lock "#{out}/.buchungen.lock" # läuft schon ein Export, bricht dieser ab (Exit-Code 3)
|
||||
history "#{out}/runs.ndjson" # eine JSON-Zeile je Lauf, auch nach Abbruch
|
||||
notify DataPipeline::Slack.new(channel: '#etl') if ENV['SLACK_TOKEN'] # Meldung nur bei Problemen
|
||||
|
||||
read container(:csv, file: "#{DATA}/buchungen.csv", csv: { col_sep: ';' })
|
||||
|
||||
@@ -662,10 +661,7 @@ DataPipeline.new do
|
||||
write container(:csv, file: "#{out}/buchungen_sap.csv", encoding: 'Windows-1252', csv: { col_sep: ';' },
|
||||
columns: %w[belnr buzei datum konto shkzg dmbtr buchungstext])
|
||||
|
||||
on_finish do |stats|
|
||||
puts "#{stats.name}: #{stats}"
|
||||
slack&.send_message("*#{stats.name}*: #{stats}") unless stats.ok?
|
||||
end
|
||||
on_finish { |stats| puts "#{stats.name}: #{stats}" }
|
||||
end.run
|
||||
```
|
||||
|
||||
@@ -679,7 +675,7 @@ end.run
|
||||
| `totals`, `reconcile` | Summen werden exakt gerechnet; eine Abweichung bricht vor dem Schreiben ab (6.4) |
|
||||
| `upsert: [belnr, buzei]` | ein wiederholter Lauf aktualisiert dieselben Zeilen, statt sie doppelt anzulegen (4.2) |
|
||||
| `encoding: 'Windows-1252'` | Zeichensatz der Zieldatei (4.4) |
|
||||
| `slack&.send_message` | Meldung nur bei Problemen und nur, wenn `SLACK_TOKEN` gesetzt ist (6.8) |
|
||||
| `notify DataPipeline::Slack…` | Meldung nur bei Problemen und nur, wenn `SLACK_TOKEN` gesetzt ist (6.8) |
|
||||
|
||||
```text
|
||||
INFO start
|
||||
@@ -765,6 +761,7 @@ Tippfehler fallen also sofort auf.
|
||||
| `on_error(:abort \| :skip \| :collect, to: storage)` | Umgang mit Fehlern in Schritten |
|
||||
| `totals 'spalte', …` | Summen über Quelle und Ergebnis |
|
||||
| `reconcile { \|stats\| … }` | Abgleich vor dem Schreiben; eine Meldung bricht ab |
|
||||
| `notify(notifier, on: :problems)`, `notifier(:name)` | Bericht an Webhook, Slack oder E-Mail (6.8) |
|
||||
| `on_finish { \|stats\| … }` | nach jedem Lauf, auch nach einem Abbruch |
|
||||
| `name(str)`, `version(str)` | Name und Version des Laufs für Log, Stats und Historie |
|
||||
|
||||
@@ -1137,7 +1134,8 @@ Gemfile der Anwendung, die das Plugin nutzt:
|
||||
| `:postgres` | `pg` |
|
||||
| `:sqlite` | `sqlite3` |
|
||||
| `:excel` | `roo` (lesen), `caxlsx` (schreiben) |
|
||||
| `:rest`, `DataPipeline::Slack` | `http` |
|
||||
| `:rest`, `DataPipeline::Slack`, `DataPipeline::Webhook` | `http` |
|
||||
| `DataPipeline::Email` | `net-smtp` |
|
||||
| `:csv`, `:json`, `:fixed` | – |
|
||||
|
||||
Eine CSV- oder JSON-Pipeline lädt keines dieser Gems. Fehlt ein Gem, meldet das Plugin bei der ersten Nutzung, welches
|
||||
@@ -1299,6 +1297,7 @@ bin/etl run cudo.rb --lock=data/.cudo.lock --history=data/runs.ndjson
|
||||
| `--dry-run`, `--limit=N` | für alle Pipelines des Skripts |
|
||||
| `--lock=FILE` | Sperre für das ganze Skript (6.3) |
|
||||
| `--history=FILE` | Laufhistorie für alle Pipelines des Skripts (6.6) |
|
||||
| `--notify=NAME`, `--notify-on=WHEN` | ein Bericht für alle Pipelines des Skripts an den Notifier `NAME` aus der Verbindungsdatei; `WHEN` = `problems` (Default), `failure`, `always` (6.8) |
|
||||
| `--verbose`, `--quiet` | Log-Level Debug bzw. Warnung (Default: Info) |
|
||||
| jede andere `--name=wert` | `params[:name]` und unverändert an das Skript, z. B. `--out=data`, `--incremental=1` |
|
||||
|
||||
@@ -1423,81 +1422,181 @@ Eigene Schritte mit Nebenwirkungen (API-Aufrufe in einem `map`) fragen `dry_run?
|
||||
|
||||
`limit n` verarbeitet nur die ersten n Quellzeilen; im Streaming wird das Lesen dann beendet.
|
||||
|
||||
### 6.8 Benachrichtigung über Slack
|
||||
### 6.8 Benachrichtigungen
|
||||
|
||||
`DataPipeline::Slack` schickt Text über die Slack-API `chat.postMessage`. Einrichten:
|
||||
|
||||
1. In Slack eine App anlegen (api.slack.com/apps), unter *OAuth & Permissions* den Bot-Scope `chat:write` vergeben,
|
||||
die App im Workspace installieren und den *Bot User OAuth Token* (`xoxb-…`) kopieren.
|
||||
2. Die App in den Ziel-Channel einladen (`/invite @appname`), sonst antwortet Slack mit `not_in_channel`.
|
||||
3. Das Gem `http` ins Gemfile der Anwendung aufnehmen (4.10).
|
||||
4. Den Token als Umgebungsvariable `SLACK_TOKEN` setzen oder mit `token:` übergeben; nicht ins Skript schreiben.
|
||||
`notify` schickt nach einem Lauf einen Bericht an einen Notifier: einen Webhook, Slack oder E-Mail. Ein Fehler beim
|
||||
Senden wird nur geloggt; Ergebnis und Exit-Code bleiben unverändert.
|
||||
|
||||
```ruby
|
||||
slack = DataPipeline::Slack.new # Token aus ENV['SLACK_TOKEN'], Channel '#general'
|
||||
slack = DataPipeline::Slack.new(channel: '#etl') # anderer Channel (Name oder ID wie 'C0123ABCD')
|
||||
slack = DataPipeline::Slack.new(channel: '#etl', token: ENV.fetch('SLACK_ETL_TOKEN')) # eigener Token je Anwendung
|
||||
|
||||
slack.send_message('Test aus der Pipeline', verbose: true) # verbose: Antwort von Slack als JSON auf $stdout
|
||||
```
|
||||
|
||||
| Option | Default | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `token:` | `ENV['SLACK_TOKEN']` | Bot-Token; fehlt er, bricht schon `Slack.new` mit `ArgumentError` ab |
|
||||
| `channel:` | `'#general'` | Ziel-Channel als Name oder ID |
|
||||
|
||||
`send_message(text, verbose: false)` liefert die Antwort von Slack als Hash. Meldet Slack einen Fehler (z. B.
|
||||
`invalid_auth`, `channel_not_found`, `not_in_channel`), wirft die Methode eine Exception mit diesem Code. In
|
||||
`on_finish` wird sie nur geloggt (`on_finish: RuntimeError: Slack#send_message failed: …`), der Lauf und sein
|
||||
Exit-Code bleiben unverändert. Es gibt keine Wiederholung bei Netzwerkfehlern.
|
||||
|
||||
Eine ausführliche Meldung mit Status, Zählern, Laufzeit und den ersten verworfenen Zeilen. Der Notifier wird vor der
|
||||
Pipeline angelegt, damit ein fehlender Token schon beim Start auffällt. Slack setzt `*fett*`, `_kursiv_` und
|
||||
`` `code` `` um.
|
||||
|
||||
```ruby
|
||||
slack = DataPipeline::Slack.new(channel: '#etl')
|
||||
|
||||
DataPipeline.new do
|
||||
name 'cudo_export'
|
||||
notify DataPipeline::Webhook.new(url: ENV['ALERT_URL'], secret: ENV['ALERT_SECRET']) # bei Problemen
|
||||
notify DataPipeline::Slack.new(channel: '#etl', mention: '<!here>'), on: :failure
|
||||
notify DataPipeline::Email.new(to: 'buchhaltung@example.com'), on: :always
|
||||
notify notifier(:alerts) # aus der Verbindungsdatei
|
||||
# … read, Schritte, write …
|
||||
|
||||
on_finish do |stats|
|
||||
next if stats.ok? && stats.warnings.zero? # nur melden, wenn etwas zu tun ist
|
||||
|
||||
status = if stats.error then ':red_circle: abgebrochen'
|
||||
elsif stats.rejected.positive? then ':large_orange_circle: mit verworfenen Zeilen'
|
||||
else ':large_yellow_circle: mit Warnungen'
|
||||
end
|
||||
lines = ["*Buchungsexport #{stats.version}* #{status}#{' (Dry-Run)' if stats.dry_run}",
|
||||
"Zeitraum #{params[:from]} – #{params[:till]}, Start #{stats.started_at.strftime('%d.%m.%Y %H:%M')}, " \
|
||||
"Dauer #{stats.duration}s",
|
||||
"gelesen #{stats.read} · geschrieben #{stats.written} · verworfen #{stats.rejected} · Warnungen #{stats.warnings}"]
|
||||
lines << "Fehler: `#{stats.error.class}: #{stats.error.message}`" if stats.error
|
||||
stats.errors.first(5).each do |e|
|
||||
lines << "• #{e[:step]}: #{e[:error]} – Beleg #{e[:row]['belnr']}"
|
||||
end
|
||||
lines << "… und #{stats.rejected - 5} weitere, siehe data/fehler.csv" if stats.rejected > 5
|
||||
|
||||
slack.send_message(lines.join("\n"))
|
||||
end
|
||||
end.run
|
||||
```
|
||||
|
||||
So sieht die Meldung in Slack aus:
|
||||
| Option von `notify` | Wirkung |
|
||||
| --- | --- |
|
||||
| `on: :problems` (Default) | bei Abbruch, verworfenen Zeilen oder Warnungen |
|
||||
| `on: :failure` | nur bei Abbruch |
|
||||
| `on: :always` | nach jedem Lauf |
|
||||
| `title:` | Titel des Berichts, Default der Name der Pipeline |
|
||||
| `dry_run: true` | auch nach einem Dry-Run senden (Default: nicht) |
|
||||
| `include_rows: true` | die verworfenen Zeilen mitsenden; Default: nur Schritt und Meldung, weil Zeilen Kundendaten enthalten können |
|
||||
|
||||
```text
|
||||
Buchungsexport 1.0 🟠 mit verworfenen Zeilen
|
||||
Zeitraum 2025-11-01 – 2025-11-30, Start 01.12.2025 06:00, Dauer 4.213s
|
||||
gelesen 1250 · geschrieben 1243 · verworfen 7 · Warnungen 0
|
||||
• validate#1: konto: missing – Beleg 5100000123
|
||||
• validate#1: dmbtr: not decimal – Beleg 5100000124
|
||||
• validate#2: Betrag 0 – Beleg 5100000130
|
||||
• validate#1: shkzg: not in ["S", "H"] – Beleg 5100000131
|
||||
• validate#1: konto: format /\A\d+\z/ – Beleg 5100000140
|
||||
… und 2 weitere, siehe data/fehler.csv
|
||||
**Bericht** – Jeder Notifier bekommt einen `DataPipeline::Report` mit den Stats eines oder mehrerer Läufe:
|
||||
|
||||
| Methode | Inhalt |
|
||||
| --- | --- |
|
||||
| `event` | `run.failed` (Abbruch), `run.rejected` (verworfene Zeilen), `run.warnings`, `run.succeeded`; bei mehreren Läufen der schlimmste |
|
||||
| `title`, `status`, `subject` | z. B. `cudo_export`, `mit verworfenen Zeilen`, `cudo_export: mit verworfenen Zeilen` |
|
||||
| `lines`, `to_s` | Text je Lauf: Zähler, Start, Dauer, die ersten 5 Fehler (ohne Zeilen) |
|
||||
| `to_h(include_rows: false)` | JSON-Payload für Webhooks (unten) |
|
||||
| `id`, `host`, `params`, `runs`, `error` | eindeutige Kennung, Rechner, Parameter der Kommandozeile, Stats, Fehler ausserhalb eines Laufs |
|
||||
|
||||
**Ein Bericht für das ganze Skript** – `notify` meldet jede Pipeline einzeln. Für einen Sammelbericht über alle
|
||||
Pipelines eines Skripts gibt es die Kommandozeile; der Notifier kommt aus der Verbindungsdatei:
|
||||
|
||||
```sh
|
||||
bin/etl run cudo.rb --notify=alerts # bei Problemen (Default)
|
||||
bin/etl run cudo.rb --notify=alerts --notify-on=always
|
||||
```
|
||||
|
||||
`params` gibt es im Pipeline-Block; ausserhalb heisst es `DataPipeline.params`.
|
||||
Bricht das Skript selbst ab (z. B. ein Fehler vor der ersten Pipeline), meldet der Bericht `run.failed` mit diesem
|
||||
Fehler. Bei belegter Sperre (Exit-Code `3`) und im Dry-Run wird nichts gesendet.
|
||||
|
||||
#### Webhook
|
||||
|
||||
`DataPipeline::Webhook` sendet den Bericht als JSON per `POST`.
|
||||
|
||||
| Option | Default | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `url:` | – | Ziel, Pflicht |
|
||||
| `secret:` | – | signiert den Body (unten) |
|
||||
| `headers:` | `{}` | weitere Header, z. B. `{ 'Authorization' => "Bearer #{ENV['TOKEN']}" }` |
|
||||
| `timeout:` | 10 | Sekunden je Anfrage |
|
||||
| `retries:`, `retry_wait:` | 3, 2 | Wiederholung bei 429, 5xx und Netzwerkfehlern, Wartezeit `retry_wait` × Versuch; `Retry-After` wird beachtet; andere 4xx brechen sofort ab |
|
||||
|
||||
```json
|
||||
{"schema":1,"id":"3f6c2a1e-…","event":"run.rejected","title":"cudo_export","host":"etl01",
|
||||
"params":{"from":"2026-10-08"},"error":null,
|
||||
"runs":[{"name":"cudo_export","version":"1.0","started_at":"2026-10-09T06:00:00.123+02:00","duration":4.213,
|
||||
"read":1250,"written":1243,"rejected":7,"warnings":0,"ok":false,"error":null,
|
||||
"totals":{"dmbtr":{"read":"98231.4","written":"97810.1"}},
|
||||
"errors":[{"step":"validate#1","error":"konto: missing"}]}]}
|
||||
```
|
||||
|
||||
`schema: 1` ist ein Versprechen: Felder können dazukommen, eine Änderung bestehender Felder hiesse `schema: 2`. Jede
|
||||
Anfrage trägt die Header `X-DataPipeline-Event` und `X-DataPipeline-Delivery` (die `id`, bei Wiederholungen gleich;
|
||||
damit erkennt der Empfänger Doppelte). Mit `secret:` kommen dazu:
|
||||
|
||||
```text
|
||||
X-DataPipeline-Timestamp: 1791525600
|
||||
X-DataPipeline-Signature: sha256=<HMAC-SHA256(secret, "<timestamp>.<body>") hexadezimal>
|
||||
```
|
||||
|
||||
Der Empfänger prüft Signatur und Alter (gegen wiederholt eingespielte Anfragen); in Ruby z. B.:
|
||||
|
||||
```ruby
|
||||
ok = DataPipeline::Webhook.valid_signature?(secret: ENV['ALERT_SECRET'], body: request.raw_post,
|
||||
timestamp: request.headers['X-DataPipeline-Timestamp'],
|
||||
signature: request.headers['X-DataPipeline-Signature'],
|
||||
max_age: 300)
|
||||
```
|
||||
|
||||
#### Slack
|
||||
|
||||
`DataPipeline::Slack` meldet einen Bericht als Statuszeile mit Symbol (🔴 abgebrochen, 🟠 verworfene Zeilen,
|
||||
🟡 Warnungen, 🟢 ok) und darunter den Text je Lauf. `&`, `<` und `>` aus Daten und Meldungen werden maskiert.
|
||||
|
||||
Zwei Wege zu Slack:
|
||||
|
||||
| Weg | Einrichten | Aufruf |
|
||||
| --- | --- | --- |
|
||||
| Incoming Webhook (einfacher) | Slack-App anlegen (api.slack.com/apps), *Incoming Webhooks* aktivieren, Webhook für den Channel erzeugen | `Slack.new(webhook: ENV['SLACK_WEBHOOK'])` |
|
||||
| Bot-Token | Slack-App mit Bot-Scope `chat:write` installieren, *Bot User OAuth Token* (`xoxb-…`) kopieren, App in den Channel einladen (`/invite @appname`) | `Slack.new(channel: '#etl')`, Token aus `ENV['SLACK_TOKEN']` |
|
||||
|
||||
| Option | Default | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `webhook:` | – | URL eines Incoming Webhook; dann braucht es weder Token noch Channel |
|
||||
| `token:` | `ENV['SLACK_TOKEN']` | Bot-Token; ohne Token und ohne Webhook bricht schon `Slack.new` mit `ArgumentError` ab |
|
||||
| `channel:` | `'#general'` | Channel als Name oder ID (nur mit Token) |
|
||||
| `mention:` | – | z. B. `'<!here>'` oder `'<!subteam^ID>'`, nur bei `run.failed` |
|
||||
| `timeout:`, `retries:`, `retry_wait:` | 10, 3, 2 | wie beim Webhook |
|
||||
|
||||
Eigener Text geht weiter mit `send_message`, z. B. in `on_finish`:
|
||||
|
||||
```ruby
|
||||
slack = DataPipeline::Slack.new(channel: '#etl')
|
||||
slack.send_message('Monatsabschluss exportiert', verbose: true) # verbose: Antwort von Slack auf $stdout
|
||||
```
|
||||
|
||||
Meldet Slack einen Fehler (`invalid_auth`, `channel_not_found`, `not_in_channel`), wirft der Aufruf
|
||||
`DataPipeline::Slack::Error` mit diesem Code.
|
||||
|
||||
#### E-Mail
|
||||
|
||||
`DataPipeline::Email` sendet den Bericht als Text-Mail (UTF-8) über SMTP. Betreff: `[etl] <Titel>: <Status>`. Die
|
||||
Zugangsdaten kommen aus der Umgebung oder als Option:
|
||||
|
||||
| Option | Default | Bedeutung |
|
||||
| --- | --- | --- |
|
||||
| `to:` | – | eine Adresse oder ein Array, Pflicht |
|
||||
| `from:` | `ENV['SMTP_FROM']`, sonst `etl@<rechner>` | Absender |
|
||||
| `host:`, `port:` | `ENV['SMTP_HOST']` bzw. `localhost`; `ENV['SMTP_PORT']` bzw. 587 | SMTP-Server |
|
||||
| `username:`, `password:` | `ENV['SMTP_USERNAME']`, `ENV['SMTP_PASSWORD']` | Anmeldung (LOGIN); ohne Benutzer keine Anmeldung |
|
||||
| `starttls:` | `:auto` | `:auto` (wenn der Server es anbietet), `:always` oder `false` (dann Default-Port 25) |
|
||||
| `tls:` | `false` | `true` = TLS von Anfang an (Port 465) |
|
||||
| `subject_prefix:` | `'[etl] '` | Anfang des Betreffs |
|
||||
| `timeout:` | 15 | Sekunden für Verbindung und Antwort |
|
||||
|
||||
Mit `include_rows: true` stehen die verworfenen Zeilen als JSON am Ende der Mail. Eigener Text:
|
||||
`DataPipeline::Email.new(to: …).send_mail('Betreff', "Text")`. Ein Fehler beim Senden wirft
|
||||
`DataPipeline::Email::Error`. Das Plugin braucht das Gem `net-smtp`.
|
||||
|
||||
#### Notifier in der Verbindungsdatei
|
||||
|
||||
Wie Storages lassen sich Notifier in `config/connections.yml` ablegen (6.2); `type` ist `webhook`, `slack` oder
|
||||
`email`, die anderen Schlüssel sind die Optionen:
|
||||
|
||||
```yaml
|
||||
alerts:
|
||||
type: webhook
|
||||
url: <%= ENV['ALERT_URL'] %>
|
||||
secret: <%= ENV['ALERT_SECRET'] %>
|
||||
buchhaltung_mail:
|
||||
type: email
|
||||
to: [buchhaltung@example.com, etl@example.com]
|
||||
host: smtp.example.com
|
||||
username: etl@example.com
|
||||
password: <%= ENV['SMTP_PASSWORD'] %>
|
||||
```
|
||||
|
||||
```ruby
|
||||
notify notifier(:buchhaltung_mail), on: :always
|
||||
```
|
||||
|
||||
#### Eigene Notifier
|
||||
|
||||
Ein Notifier ist eine Klasse mit Basisklasse `DataPipeline::Notifier` und einer Methode
|
||||
`deliver(report, include_rows: false)`, z. B. für Microsoft Teams oder ein Ticketsystem:
|
||||
|
||||
```ruby
|
||||
require 'http'
|
||||
|
||||
class TeamsNotifier < DataPipeline::Notifier
|
||||
def initialize(url:) = @url = url
|
||||
|
||||
def deliver report, include_rows: false
|
||||
HTTP.post(@url, json: { title: report.subject, text: report.lines.join("\n\n") })
|
||||
end
|
||||
end
|
||||
|
||||
notify TeamsNotifier.new(url: ENV['TEAMS_WEBHOOK']), on: :failure
|
||||
```
|
||||
|
||||
### 6.9 Fehlersuche
|
||||
|
||||
@@ -1523,11 +1622,19 @@ schnellen Blick auf das Ergebnis.
|
||||
|
||||
## 7. Umstieg
|
||||
|
||||
### 7.0 Von 3.0 auf 3.1
|
||||
### 7.0 Von 3.0 auf 3.1 und 3.2
|
||||
|
||||
3.1 ist abwärtskompatibel. Anders als in 3.0: SQLite schreibt im Streaming (`write_row`) nicht mehr jede Zeile
|
||||
einzeln, sondern je 1000 Zeilen in einer Transaktion (4.3). Wer während eines Laufs mit einer zweiten Verbindung
|
||||
auf die Zieldatei schaut, sieht die Zeilen also paketweise. Das Gem steht jetzt unter der BSD-3-Clause-Lizenz.
|
||||
3.1 und 3.2 sind abwärtskompatibel. Anders als in 3.0:
|
||||
|
||||
| Thema | bis 3.0 | jetzt | seit |
|
||||
| --- | --- | --- | --- |
|
||||
| SQLite, `write_row` im Streaming | jede Zeile einzeln | je 1000 Zeilen in einer Transaktion (4.3); eine zweite Verbindung sieht die Zeilen paketweise | 3.1 |
|
||||
| Lizenz | nicht festgelegt | BSD-3-Clause | 3.1 |
|
||||
| Slack-Fehler | `RuntimeError` | `DataPipeline::Slack::Error` (weiterhin ein `StandardError`) | 3.2 |
|
||||
| `Slack.new` | lädt `http` erst beim Senden | lädt `http` sofort; Timeout 10 s und Wiederholung bei 429/5xx | 3.2 |
|
||||
| `send_message(…, verbose: true)` | Antwort als eingerücktes JSON | Antwort wie von Slack geliefert | 3.2 |
|
||||
|
||||
Selbstgebaute Slack-Meldungen in `on_finish` laufen weiter; einfacher ist `notify` (6.8).
|
||||
|
||||
### 7.1 Von 2.x auf 3.0
|
||||
|
||||
@@ -1584,7 +1691,7 @@ ersten Zeilen sollten gleich sein.
|
||||
| [lib/datapipeline/lock.rb](lib/datapipeline/lock.rb) | Sperre gegen parallele Läufe |
|
||||
| [lib/datapipeline/connections.rb](lib/datapipeline/connections.rb) | Verbindungsdatei |
|
||||
| [lib/datapipeline/storage/](lib/datapipeline/storage/) | `storage_plugin.rb` (Basisklasse) und je Plugin eine Datei |
|
||||
| [lib/datapipeline/notification/slack.rb](lib/datapipeline/notification/slack.rb) | Slack |
|
||||
| [lib/datapipeline/notification/](lib/datapipeline/notification/) | `notifier.rb` (Report, Basisklasse), `webhook.rb`, `slack.rb`, `email.rb` |
|
||||
| [lib/datapipeline/version.rb](lib/datapipeline/version.rb) | `DataPipeline::VERSION` |
|
||||
| [exe/etl](exe/etl) | Kommando `etl`; [bin/etl](bin/etl) startet es im Repository mit dem Bundle |
|
||||
| [examples/](examples/) | Beispiele aus Kapitel 2, Daten in `examples/data/`, Verbindungsdatei in `examples/config/`, Demo-API |
|
||||
@@ -1592,14 +1699,14 @@ ersten Zeilen sollten gleich sein.
|
||||
|
||||
### 8.2 Tests
|
||||
|
||||
`bundle exec rake test` startet die Minitest-Suite in [test/](test/), rund 190 Fälle. Einzelne Datei:
|
||||
`bundle exec rake test` startet die Minitest-Suite in [test/](test/), rund 210 Fälle. Einzelne Datei:
|
||||
`bundle exec rake test TEST=test/core/steps_test.rb`.
|
||||
|
||||
| Ordner | Inhalt |
|
||||
| --- | --- |
|
||||
| `test/plugins` | je Plugin das gemeinsame Testkit `test/support/storage_contract.rb` (Schreiben und Zurücklesen, zeilenweise, als Quelle und Ziel) plus eigene Fälle; REST gegen einen lokalen Testserver; Laden der Gems erst bei Nutzung |
|
||||
| `test/plugins` | je Plugin das gemeinsame Testkit `test/support/storage_contract.rb` (Schreiben und Zurücklesen, zeilenweise, als Quelle und Ziel) plus eigene Fälle; REST, Webhook und Slack gegen einen lokalen HTTP-Testserver, E-Mail gegen einen SMTP-Testserver (`test/support/`); Laden der Gems erst bei Nutzung |
|
||||
| `test/core` | DSL, Schritte und Kurzschritte, Batch und Streaming, Fehlerbehandlung, Layouts, Sperre, Verbindungen, Summen, Historie, Wasserzeichen |
|
||||
| `test/cli` | `bin/etl` als Prozess: Parameter, Dry-Run, Sperre, Historie, Exit-Codes |
|
||||
| `test/cli` | `bin/etl` als Prozess: Parameter, Dry-Run, Sperre, Historie, Sammelbericht `--notify`, Exit-Codes |
|
||||
| `test/examples` | die Beispiele aus Kapitel 2: Code in der Doku = Datei, Lauf und Ergebnis; REST gegen die Demo-API |
|
||||
|
||||
Eine `.env` wird in Tests nicht geladen; kein Test erreicht eine Produktivdatenbank. Postgres-Tests legen je Test eine
|
||||
@@ -1620,10 +1727,10 @@ Ein eigenes Plugin lässt sich mit `MemoryStorage` aus `test/test_helper.rb` als
|
||||
|
||||
- Version in [lib/datapipeline/version.rb](lib/datapipeline/version.rb), Änderungen in [CHANGELOG.md](CHANGELOG.md)
|
||||
(neueste oben). Neue Funktionen erhöhen die mittlere Stelle, Breaking Changes die erste (wie 3.0, 7.1).
|
||||
- Jede Version bekommt ein Tag `vX.Y.Z` (`git tag -a v3.1.0 -m "datapipeline 3.1.0" && git push origin v3.1.0`).
|
||||
- Jede Version bekommt ein Tag `vX.Y.Z` (`git tag -a v3.2.0 -m "datapipeline 3.2.0" && git push origin v3.2.0`).
|
||||
Anwendungen binden das Gem mit einer Versionsgrenze ein (`'~> 3.1'`, 1.2) oder fest auf ein Tag
|
||||
(`tag: 'v3.1.0'`; dann geht `local.datapipeline` nicht).
|
||||
- Das `Gemfile` bindet das Gem über `gemspec` ein und ergänzt die Gruppen `plugins` (`pg`, `sqlite3`, `roo`,
|
||||
(`tag: 'v3.2.0'`; dann geht `local.datapipeline` nicht).
|
||||
- Das `Gemfile` bindet das Gem über `gemspec` ein und ergänzt die Gruppen `plugins` (`pg`, `sqlite3`, `roo`, `net-smtp`,
|
||||
`caxlsx`, `http`) und `test`; `Gemfile.lock` ist versioniert. Ohne Plugin-Gems: `bundle config set without plugins`.
|
||||
- Gem bauen: `gem build datapipeline.gemspec`.
|
||||
- Lizenz: BSD-3-Clause, siehe [LICENSE](LICENSE); die Datei ist Teil des Gems.
|
||||
@@ -1640,6 +1747,5 @@ Offene Punkte, Reihenfolge = empfohlene Umsetzung:
|
||||
schreiben), `etl list` (Pipelines, Wasserzeichen, letzter Lauf aus der Historie), `--json` für die Stats.
|
||||
2. **Qualität:** CI auf git.wicom.li mit `rake test` und Postgres-Service, RuboCop, `DataPipeline::Testing` mit
|
||||
`MemoryStorage` als öffentliche Testhilfe für Anwendungen.
|
||||
3. **Slack:** fertige Meldung aus den Stats (`notify slack, on: :problems`), Timeout und Wiederholung bei `429`,
|
||||
Maskierung von `&`, `<`, `>`, Incoming Webhooks.
|
||||
3. **Benachrichtigungen:** Microsoft Teams als mitgelieferter Notifier, HTML-Mail.
|
||||
4. **Weitere Plugins:** SFTP-Ablage.
|
||||
|
||||
Reference in New Issue
Block a user