-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for streaming async events via HTTP serverside events.
- `GET /api/events?type=error` opens a long-lived HTTP server side event connection that streams error messages. - async (typically SMTP) errors are now streamed to the frontend and disaplyed as an error toast on the admin UI.
- Loading branch information
Showing
10 changed files
with
193 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"log" | ||
"time" | ||
|
||
"github.com/labstack/echo/v4" | ||
) | ||
|
||
// handleEventStream serves an endpoint that never closes and pushes a | ||
// live event stream (text/event-stream) such as a error messages. | ||
func handleEventStream(c echo.Context) error { | ||
var ( | ||
app = c.Get("app").(*App) | ||
) | ||
|
||
h := c.Response().Header() | ||
h.Set(echo.HeaderContentType, "text/event-stream") | ||
h.Set(echo.HeaderCacheControl, "no-store") | ||
h.Set(echo.HeaderConnection, "keep-alive") | ||
|
||
// Subscribe to the event stream with a random ID. | ||
id := fmt.Sprintf("api:%v", time.Now().UnixNano()) | ||
sub, err := app.events.Subscribe(id) | ||
if err != nil { | ||
log.Fatalf("error subscribing to events: %v", err) | ||
} | ||
|
||
ctx := c.Request().Context() | ||
for { | ||
select { | ||
case e := <-sub: | ||
b, err := json.Marshal(e) | ||
if err != nil { | ||
app.log.Printf("error marshalling event: %v", err) | ||
continue | ||
} | ||
|
||
fmt.Printf("data: %s\n\n", b) | ||
|
||
c.Response().Write([]byte(fmt.Sprintf("retry: 3000\ndata: %s\n\n", b))) | ||
c.Response().Flush() | ||
|
||
case <-ctx.Done(): | ||
// On HTTP connection close, unsubscribe. | ||
app.events.Unsubscribe(id) | ||
return nil | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// Package events implements a simple event broadcasting mechanism | ||
// for usage in broadcasting error messages, postbacks etc. various | ||
// channels. | ||
package events | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"sync" | ||
) | ||
|
||
const ( | ||
TypeError = "error" | ||
) | ||
|
||
// Event represents a single event in the system. | ||
type Event struct { | ||
ID string `json:"id"` | ||
Type string `json:"type"` | ||
Message string `json:"message"` | ||
Data interface{} `json:"data"` | ||
Channels []string `json:"-"` | ||
} | ||
|
||
type Events struct { | ||
subs map[string]chan Event | ||
sync.RWMutex | ||
} | ||
|
||
// New returns a new instance of Events. | ||
func New() *Events { | ||
return &Events{ | ||
subs: make(map[string]chan Event), | ||
} | ||
} | ||
|
||
// Subscribe returns a channel to which the given event `types` are streamed. | ||
// id is the unique identifier for the caller. A caller can only register | ||
// for subscription once. | ||
func (ev *Events) Subscribe(id string) (chan Event, error) { | ||
ev.Lock() | ||
defer ev.Unlock() | ||
|
||
if ch, ok := ev.subs[id]; ok { | ||
return ch, nil | ||
} | ||
|
||
ch := make(chan Event, 100) | ||
ev.subs[id] = ch | ||
|
||
return ch, nil | ||
} | ||
|
||
// Unsubscribe unsubscribes a subscriber (obviously). | ||
func (ev *Events) Unsubscribe(id string) { | ||
ev.Lock() | ||
defer ev.Unlock() | ||
delete(ev.subs, id) | ||
} | ||
|
||
// Publish publishes an event to all subscribers. | ||
func (ev *Events) Publish(e Event) error { | ||
ev.Lock() | ||
defer ev.Unlock() | ||
|
||
for _, ch := range ev.subs { | ||
select { | ||
case ch <- e: | ||
default: | ||
return fmt.Errorf("event queue full for type: %s", e.Type) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// This implements an io.Writer specifically for receiving error messages | ||
// mirrored (io.MultiWriter) from error log writing. | ||
type wri struct { | ||
ev *Events | ||
} | ||
|
||
func (w *wri) Write(b []byte) (n int, err error) { | ||
// Only broadcast error messages. | ||
if !bytes.Contains(b, []byte("error")) { | ||
return 0, nil | ||
} | ||
|
||
w.ev.Publish(Event{ | ||
Type: TypeError, | ||
Message: string(b), | ||
}) | ||
|
||
return len(b), nil | ||
} | ||
|
||
func (ev *Events) ErrWriter() io.Writer { | ||
return &wri{ev: ev} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters