Skip to content
This repository was archived by the owner on Dec 9, 2024. It is now read-only.

Commit 31799d1

Browse files
authored
Plugin System (#330). Closes #147
1 parent 4c2ba40 commit 31799d1

File tree

20 files changed

+905
-161
lines changed

20 files changed

+905
-161
lines changed

Gopkg.lock

Lines changed: 26 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ yet ready for production applications.*
3434
1. [Subscriptions](#subscriptions)
3535
1. [Events API](#events-api)
3636
1. [Configuration API](#configuration-api)
37+
1. [System Events](#system-events)
38+
1. [Plugin System](#plugin-system)
3739
1. [Client Libraries](#client-libraries)
3840
1. [Versioning](#versioning)
3941
1. [Comparison](#comparison)
@@ -281,13 +283,6 @@ the data block is base64 encoded.
281283

282284
`invoke` is a built-in type of event allowing to call functions synchronously.
283285

284-
#### System Events
285-
286-
The Event Gateway emits system events allowing to react on internal events. Currently, only one internal event is
287-
emitted. `gateway.info.functionError` happens when function invocation failed.
288-
289-
If you are looking for more system events, please comment [the corresponding issue](https://github.com/serverless/event-gateway/issues/215).
290-
291286
### Emit a Custom Event
292287

293288
Creating a subscription requires `path` property (by default it's "/"). `path` indicates path under which you can push.
@@ -551,6 +546,52 @@ Dummy endpoint (always returning `200 OK` status code) for checking if the event
551546

552547
`GET <Configuration API URL>/v1/status`
553548

549+
## System Events
550+
551+
System Events are special type of events emitted by the Event Gateway instance. They are emitted on each stage of event
552+
processing flow starting from receiving event to function invocation end. Those events are:
553+
554+
- `gateway.event.received` - the event is emitted when an event was received by Events API. Data fields:
555+
- `event` - event payload
556+
- `path` - Events API path
557+
- `headers` - HTTP request headers
558+
- `gateway.function.invoking` - the event emitted before invoking a function. Data fields:
559+
- `event` - event payload
560+
- `functionId` - registered function ID
561+
- `gateway.function.invoked` - the event emitted after successful function invocation. Data fields:
562+
- `event` - event payload
563+
- `functionId` - registered function ID
564+
- `result` - function response
565+
- `gateway.function.invocationFailed` - the event emitted after failed function invocation. Data fields:
566+
- `event` - event payload
567+
- `functionId` - registered function ID
568+
- `error` - invocation error
569+
570+
## Plugin System
571+
572+
The Event Gateway is built with extensibility in mind. Built-in plugin system allows reacting on system events and
573+
manipulate how an event is processed through the Event Gateway.
574+
575+
_Current implementation supports plugins written only in Golang. We plan to support other languages in the future._
576+
577+
Plugin system is based on [go-plugin](https://github.com/hashicorp/go-plugin). A plugin needs to implement the following
578+
interface:
579+
580+
```go
581+
type Reacter interface {
582+
Subscriptions() []Subscription
583+
React(event event.Event) error
584+
}
585+
```
586+
587+
`Subscription` model indicates the event that plugin subscribes to and the subscription type. A subscription can be either
588+
sync or async. Sync (blocking) subscription means that in case of error returned from `React` method the event won't be
589+
further processed by the Event Gateway.
590+
591+
`React` method is called for every system event that plugin subscribed to.
592+
593+
For more details, see [the example plugin](plugin/example).
594+
554595
## Client Libraries
555596

556597
- [FDK for Node.js](https://github.com/serverless/fdk)

api/events.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,33 @@ import (
66
"time"
77

88
"github.com/rs/cors"
9-
"github.com/serverless/event-gateway/internal/cache"
109
"github.com/serverless/event-gateway/internal/httpapi"
11-
"github.com/serverless/event-gateway/internal/metrics"
12-
"github.com/serverless/event-gateway/router"
1310
)
1411

15-
// StartEventsAPI creates a new gateway endpoint and listens for requests.
16-
func StartEventsAPI(config httpapi.Config) httpapi.Server {
17-
targetCache := cache.NewTarget("/serverless-event-gateway", config.KV, config.Log)
18-
router := router.New(targetCache, metrics.DroppedPubSubEvents, config.Log)
19-
router.StartWorkers()
12+
// EventsAPIConfig stores configuration for Events API. Events API expects cofigured router. That's why new
13+
// configuration object is needed.
14+
type EventsAPIConfig struct {
15+
httpapi.Config
16+
Router http.Handler
17+
}
2018

19+
// StartEventsAPI creates a new gateway endpoint and listens for requests.
20+
func StartEventsAPI(config EventsAPIConfig) httpapi.Server {
2121
handler := &http.Server{
2222
Addr: ":" + strconv.Itoa(int(config.Port)),
23-
Handler: cors.AllowAll().Handler(router),
23+
Handler: cors.AllowAll().Handler(config.Router),
2424
ReadTimeout: 3 * time.Second,
2525
WriteTimeout: 3 * time.Second,
2626
}
2727

2828
server := httpapi.Server{
29-
Config: config,
29+
Config: config.Config,
3030
HTTPHandler: handler,
3131
}
3232

3333
config.ShutdownGuard.Add(1)
3434
go func() {
3535
server.Listen()
36-
router.Drain()
3736
config.ShutdownGuard.Done()
3837
}()
3938

cmd/event-gateway/main.go

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,25 @@ import (
1515
"go.uber.org/zap/zapcore"
1616

1717
"github.com/serverless/event-gateway/api"
18+
"github.com/serverless/event-gateway/internal/cache"
1819
"github.com/serverless/event-gateway/internal/embedded"
1920
"github.com/serverless/event-gateway/internal/httpapi"
2021
"github.com/serverless/event-gateway/internal/metrics"
2122
"github.com/serverless/event-gateway/internal/sync"
23+
"github.com/serverless/event-gateway/plugin"
24+
"github.com/serverless/event-gateway/router"
2225
)
2326

2427
var version = "dev"
2528

2629
func init() {
2730
etcd.Register()
31+
32+
prometheus.MustRegister(metrics.RequestDuration)
33+
prometheus.MustRegister(metrics.DroppedPubSubEvents)
2834
}
2935

36+
// nolint: gocyclo
3037
func main() {
3138
showVersion := flag.Bool("version", false, "Show version.")
3239
logLevel := zap.LevelFlag("log-level", zap.InfoLevel, `The level of logging to show after the event gateway has started. The available log levels are "debug", "info", "warn", and "err".`)
@@ -42,17 +49,16 @@ func main() {
4249
eventsPort := flag.Uint("events-port", 4000, "Port to serve events API on.")
4350
eventsTLSCrt := flag.String("events-tls-cert", "", "Path to events API TLS certificate file.")
4451
eventsTLSKey := flag.String("events-tls-key", "", "Path to events API TLS key file.")
52+
plugins := paths{}
53+
flag.Var(&plugins, "plugin", "Path to a plugin to load.")
4554
flag.Parse()
4655

4756
if *showVersion {
4857
fmt.Printf("Event Gateway version: %s\n", version)
4958
os.Exit(0)
5059
}
5160

52-
prometheus.MustRegister(metrics.RequestDuration)
53-
prometheus.MustRegister(metrics.DroppedPubSubEvents)
54-
55-
log, err := loggerConfiguration(*developmentMode, *logLevel, *logFormat).Build()
61+
log, err := logger(*developmentMode, *logLevel, *logFormat).Build()
5662
if err != nil {
5763
panic(err)
5864
}
@@ -64,10 +70,9 @@ func main() {
6470
embedded.EmbedEtcd(*embedDataDir, *embedPeerAddr, *embedCliAddr, shutdownGuard)
6571
}
6672

67-
dbHostStrings := strings.Split(*dbHosts, ",")
6873
kv, err := libkv.NewStore(
6974
store.ETCDV3,
70-
dbHostStrings,
75+
strings.Split(*dbHosts, ","),
7176
&store.Config{
7277
ConnectionTimeout: 10 * time.Second,
7378
},
@@ -76,13 +81,26 @@ func main() {
7681
log.Fatal("Cannot create KV client.", zap.Error(err))
7782
}
7883

79-
eventServer := api.StartEventsAPI(httpapi.Config{
80-
KV: kv,
81-
Log: log,
82-
TLSCrt: eventsTLSCrt,
83-
TLSKey: eventsTLSKey,
84-
Port: *eventsPort,
85-
ShutdownGuard: shutdownGuard,
84+
pluginManager := plugin.NewManager(plugins, log)
85+
err = pluginManager.Connect()
86+
if err != nil {
87+
log.Fatal("Loading plugins failed.", zap.Error(err))
88+
}
89+
90+
targetCache := cache.NewTarget("/serverless-event-gateway", kv, log)
91+
router := router.New(targetCache, pluginManager, metrics.DroppedPubSubEvents, log)
92+
router.StartWorkers()
93+
94+
eventServer := api.StartEventsAPI(api.EventsAPIConfig{
95+
Config: httpapi.Config{
96+
KV: kv,
97+
Log: log,
98+
TLSCrt: eventsTLSCrt,
99+
TLSKey: eventsTLSKey,
100+
Port: *eventsPort,
101+
ShutdownGuard: shutdownGuard,
102+
},
103+
Router: router,
86104
})
87105

88106
configServer := api.StartConfigAPI(httpapi.Config{
@@ -108,14 +126,19 @@ func main() {
108126
}
109127

110128
shutdownGuard.Wait()
129+
router.Drain()
130+
131+
if pluginManager != nil {
132+
pluginManager.Kill()
133+
}
111134
}
112135

113136
const (
114137
consoleEncoding = "console"
115138
jsonEncoding = "json"
116139
)
117140

118-
func loggerConfiguration(dev bool, level zapcore.Level, format string) zap.Config {
141+
func logger(dev bool, level zapcore.Level, format string) zap.Config {
119142
cfg := zap.Config{
120143
Level: zap.NewAtomicLevelAt(level),
121144
Development: false,
@@ -153,3 +176,14 @@ func loggerConfiguration(dev bool, level zapcore.Level, format string) zap.Confi
153176

154177
return cfg
155178
}
179+
180+
type paths []string
181+
182+
func (p *paths) String() string {
183+
return strings.Join(*p, ",")
184+
}
185+
186+
func (p *paths) Set(value string) error {
187+
*p = append(*p, value)
188+
return nil
189+
}

event/event.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package event
22

33
import (
4+
"encoding/json"
5+
"strings"
46
"time"
57

8+
"go.uber.org/zap/zapcore"
9+
610
uuid "github.com/satori/go.uuid"
711
)
812

@@ -34,3 +38,20 @@ const TypeInvoke = Type("invoke")
3438

3539
// TypeHTTP is a special type of event for sync http subscriptions.
3640
const TypeHTTP = Type("http")
41+
42+
// MarshalLogObject is a part of zapcore.ObjectMarshaler interface
43+
func (e Event) MarshalLogObject(enc zapcore.ObjectEncoder) error {
44+
enc.AddString("type", string(e.Type))
45+
enc.AddString("id", e.ID)
46+
enc.AddUint64("receivedAt", e.ReceivedAt)
47+
payload, _ := json.Marshal(e.Data)
48+
enc.AddString("data", string(payload))
49+
enc.AddString("dataType", e.DataType)
50+
51+
return nil
52+
}
53+
54+
// IsSystem indicates if th event is a system event.
55+
func (e Event) IsSystem() bool {
56+
return strings.HasPrefix(string(e.Type), "gateway.")
57+
}

event/system.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package event
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/serverless/event-gateway/functions"
7+
)
8+
9+
// SystemEventReceivedType is a system event emmited when the Event Gateway receives an event.
10+
const SystemEventReceivedType = Type("gateway.event.received")
11+
12+
// SystemEventReceivedData struct.
13+
type SystemEventReceivedData struct {
14+
Path string `json:"path"`
15+
Event Event `json:"event"`
16+
Headers http.Header `json:"header"`
17+
}
18+
19+
// SystemFunctionInvokingType is a system event emmited before invoking a function.
20+
const SystemFunctionInvokingType = Type("gateway.function.invoking")
21+
22+
// SystemFunctionInvokingData struct.
23+
type SystemFunctionInvokingData struct {
24+
FunctionID functions.FunctionID `json:"functionId"`
25+
Event Event `json:"event"`
26+
}
27+
28+
// SystemFunctionInvokedType is a system event emmited after successful function invocation.
29+
const SystemFunctionInvokedType = Type("gateway.function.invoked")
30+
31+
// SystemFunctionInvokedData struct.
32+
type SystemFunctionInvokedData struct {
33+
FunctionID functions.FunctionID `json:"functionId"`
34+
Event Event `json:"event"`
35+
Result []byte `json:"result"`
36+
}
37+
38+
// SystemFunctionInvocationFailedType is a system event emmited after successful function invocation.
39+
const SystemFunctionInvocationFailedType = Type("gateway.function.invocationFailed")
40+
41+
// SystemFunctionInvocationFailedData struct.
42+
type SystemFunctionInvocationFailedData struct {
43+
FunctionID functions.FunctionID `json:"functionId"`
44+
Event Event `json:"event"`
45+
Error []byte `json:"result"`
46+
}

0 commit comments

Comments
 (0)