Skip to content

Commit

Permalink
Convention over configuration fixes #111 (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
mantzas authored Jun 24, 2018
1 parent 6bf4941 commit 00145ac
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 253 deletions.
251 changes: 128 additions & 123 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,136 @@ Patron is a framework for creating microservices.

Patron is french for `template` or `pattern`, but it means also `boss` which we found out later (no pun intended).

The entry point of the framework is the `Service`. The `Service` uses `Components` to handle the processing of sync and async requests. The `Service` starts by default a `HTTP Component` which hosts the debug, health and metric endpoints. Any other endpoints will be added to the default `HTTP Component` as `Routes`. The service set's up by default logging with `zerolog`, tracing and metrics with `jaeger`.

Patron provides abstractions for the following functionality of the framework:

- configuration
- logging
- metrics and tracing (TBD)
- service
- components and processors
- asynchronous message processing (RabbitMQ, Kafka)
- synchronous processing (HTTP)
- service
- metrics and tracing
- logging
- configuration

## Config
## Service

The config package defines a interface that has to be implemented in order to be used inside the application.
The `Service` has the role of glueing all of the above together, which are:

- setting up logging
- setting up termination by user
- starting and stopping components
- handling component errors

The service has some default settings tha can be changed via environment variables:

- Service HTTP port, which set's up the default HTTP components port to `50000` which can be changed via the `PATRON_HTTP_DEFAULT_PORT`
- Log level, which set's up zerolog with `INFO` as log level which can be changed via the `PATRON_LOG_LEVEL`
- Tracing, which set's up jaeger tracing with
- agent address `0.0.0.0:6831`, which can be changed via `PATRON_JAEGER_AGENT`
- sampler type `probabilistic`, which can be changed via `PATRON_JAEGER_SAMPLER_TYPE`
- sampler param `0.1`, which can be changed via `PATRON_JAEGER_SAMPLER_PARAM`

### Component

A `Component` is a interface that exposes the following API:

```go
type Config interface {
Set(key string, value interface{}) error
Get(key string) (interface{}, error)
GetBool(key string) (bool, error)
GetInt64(key string) (int64, error)
GetString(key string) (string, error)
GetFloat64(key string) (float64, error)
type Component interface {
Run(ctx context.Context) error
Shutdown(ctx context.Context) error
}
```

After implementing the interface a instance has to be provided to the `Setup` method of the package in order to be used directly from the package eg `config.GetBool()`.
The above API gives the `Service` the control over a component in order to start and stop it gracefully. The framework divides the components in 2 categories:

The following implementations are provided as sub-packages:
- synchronous, which are components that follow the request/response pattern and
- asynchronous, which consume messages from a source but don't respond anything back

- env, support for env files and env vars
The following component implementations are available:

### env
- HTTP (sync)
- RabbitMQ (async)
- Kafka (async)

The env package supports getting env vars from the system. It allows further to provide a file that contain env vars, separated by a equal sign `=`, which are then set up on the environment. In order to setup config just do the following:
Adding to the above list is as easy as implementing a `Component` and a `Processor` for that component.

## Example

Setting up a new service with a HTTP `Component` is as easy as the following code:

```go
c,err := env.New({reader to the config file})
// error checking
config.Setup(c)
// Set up HTTP routes
routes := make([]sync_http.Route, 0)
routes = append(routes, sync_http.NewRoute("/", http.MethodGet, process, true))

srv, err := patron.New("test", patron.Routes(routes))
if err != nil {
log.Fatalf("failed to create service %v", err)
}

err = srv.Run()
if err != nil {
log.Fatalf("failed to create service %v", err)
}
```

The above is pretty much self-explanatory.

## Processors

### Synchronous

The implementation of the processor is responsible to create a `Request` by providing everything that is needed (Headers, Fields, decoder, raw io.Reader) pass it to the implementation by invoking the `Process` method and handle the `Response` or the `error` returned by the processor.

The sync processor package contains only a interface definition of the processor along the models needed:

```go
type Processor interface {
Process(context.Context, *Request) (*Response, error)
}
```

The `Request` model contains the following properties (which are provided when calling the "constructor" `NewRequest`)

- Headers, which may contains any headers associated with the request
- Fields, which may contain any fields associated with the request
- Raw, the raw request data (if any) in the form of a `io.Reader`
- decode, which is a function of type `encoding.Decode` that decodes the raw reader

A exported function exists for decoding the raw io.Reader in the form of

```go
Decode(v interface{}) error
```

The `Response` model contains the following properties (which are provided when calling the "constructor" `NewResponse`)

- Payload, which may hold a struct of type `interface{}`

### Asynchronous

The implementation of the async processor follows exactly the same principle as the sync processor.
The main difference is that:

- The `Request` is the `Message` and contains only data as `[]byte`
- There is no `Response`, so the processor may return a error

```go
type Processor interface {
Process(context.Context, *Message) error
}
```

Everything else is exactly the same.

## Metrics and Tracing

Tracing and metrics are provided by jaeger's implementation of the OpenTracing project.
Every component has been integrated with the above library and produces traces and metrics.
Metrics are provided with the default HTTP component at the `/metrics` route for Prometheus to scrape.
Tracing will be send to a jaeger agent which can be setup though environment variables mentioned in the config section.

## Logging

The log package is designed to be a leveled logger with field support.
Expand Down Expand Up @@ -115,117 +204,33 @@ Two methods are supported:
- Create, which creates a logger with the specified fields (or nil)
- CreateSub, which creates a sub-logger that accepts a logger and fields and creates a sub-logger with the fields merged into the new one.

## Metrics and Tracing (TBD)

## Processors

### Synchronous

The implementation of the processor is responsible to create a `Request` by providing everything that is needed (Headers, Fields, decoder, raw io.Reader) pass it to the implementation by invoking the `Process` method and handle the `Response` or the `error` returned by the processor.

The sync processor package contains only a interface definition of the processor along the models needed:

```go
type Processor interface {
Process(context.Context, *Request) (*Response, error)
}
```

The `Request` model contains the following properties (which are provided when calling the "constructor" `NewRequest`)

- Headers, which may contains any headers associated with the request
- Fields, which may contain any fields associated with the request
- Raw, the raw request data (if any) in the form of a `io.Reader`
- decode, which is a function of type `encoding.Decode` that decodes the raw reader

A exported function exists for decoding the raw io.Reader in the form of

```go
Decode(v interface{}) error
```

The `Response` model contains the following properties (which are provided when calling the "constructor" `NewResponse`)

- Payload, which may hold a struct of type `interface{}`

### Asynchronous

The implementation of the async processor follows exactly the same principle as the sync processor.
The main difference is that:

- The `Request` is the `Message` and contains only data as `[]byte`
- There is no `Response`, so the processor may return a error

```go
type Processor interface {
Process(context.Context, *Message) error
}
```

Everything else is exactly the same.

## Service

The `Service` has the role of glueing all of the above together, which are:

- setting up logging
- setting up termination by user
- starting and stopping components
- handling component errors

### Component
## Config

A `Component` is a interface that exposes the following API:
The config package defines a interface that has to be implemented in order to be used inside the application.

```go
type Component interface {
Run(ctx context.Context) error
Shutdown(ctx context.Context) error
type Config interface {
Set(key string, value interface{}) error
Get(key string) (interface{}, error)
GetBool(key string) (bool, error)
GetInt64(key string) (int64, error)
GetString(key string) (string, error)
GetFloat64(key string) (float64, error)
}
```

The above API gives the `Service` the control over a component in order to start and stop it gracefully. The framework divides the components in 2 categories:

- synchronous, which are components that follow the request/response pattern and
- asynchronous, which consume messages from a source but don't respond anything back

The following component implementations are available:
After implementing the interface a instance has to be provided to the `Setup` method of the package in order to be used directly from the package eg `config.GetBool()`.

- HTTP (sync)
- RabbitMQ (async)
- Kafka (async)
The following implementations are provided as sub-packages:

Adding to the above list is as easy as implementing a `Component` and a `Processor` for that component.
- env, support for env files and env vars

## Example
### env

Setting up a new service with a HTTP `Component` is as easy as the following code:
The env package supports getting env vars from the system. It allows further to provide a file that contain env vars, separated by a equal sign `=`, which are then set up on the environment. In order to setup config just do the following:

```go
// Set up HTTP routes
routes := make([]sync_http.Route, 0)
routes = append(routes, sync_http.NewRoute("/", http.MethodGet, indexProcessor{}))

// Create a HTTP component with the above routes
httpCp, err := sync_http.New(httprouter.CreateHandler, sync_http.Routes(routes))
if err != nil {
fmt.Print("failed to create HTTP service", err)
os.Exit(1)
}

// Create a new service
srv, err := patron.New("test", []patron.Component{httpCp})
if err != nil {
fmt.Printf("failed to create service %v", err)
os.Exit(1)
}

// Run the service
err = srv.Run()
if err != nil {
fmt.Printf("failed to create service %v", err)
os.Exit(1)
}
```

The above is pretty much self-explanatory.
c,err := env.New({reader to the config file})
// error checking
config.Setup(c)
```
2 changes: 0 additions & 2 deletions examples/.env

This file was deleted.

Loading

0 comments on commit 00145ac

Please sign in to comment.