Skip to content

Commit 0e7a495

Browse files
sakjurpapagian
andauthored
Docs: Update backend architecture contributor documentation (grafana#51172)
Co-authored-by: Sofia Papagiannaki <[email protected]>
1 parent d63ffa3 commit 0e7a495

File tree

3 files changed

+375
-96
lines changed

3 files changed

+375
-96
lines changed

contribute/architecture/backend/communication.md

+83-86
Original file line numberDiff line numberDiff line change
@@ -2,134 +2,131 @@
22

33
Grafana uses a _bus_ to pass messages between different parts of the application. All communication over the bus happens synchronously.
44

5-
> **Deprecated:** The bus has officially been deprecated, however, we're still using the command/query objects paradigms.
5+
## Commands and queries
66

7-
There are three types of messages: _events_, _commands_, and _queries_.
7+
Grafana structures arguments to [services](services.md) using a command/query
8+
separation where commands are instructions for a mutation and queries retrieve
9+
records from a service.
810

9-
## Events
11+
Services should define their methods as `func[T, U any](ctx context.Context, args T) (U, error)`.
1012

11-
An event is something that happened in the past. Since an event has already happened, you can't change it. Instead, you can react to events by triggering additional application logic to be run, whenever they occur.
13+
Each function should take two arguments. First, a `context.Context` that
14+
carries information about the tracing span, cancellation, and similar
15+
runtime information that might be relevant to the call. Secondly, `T` is
16+
a `struct` defined in the service's root package (see the instructions
17+
for [package hierarchy](package-hierarchy.md)) that contains zero or
18+
more arguments that can be passed to the method.
1219

13-
> Because they happened in the past, event names are written in past tense, such as `UserCreated`, and `OrgUpdated`.
14-
15-
### Subscribe to an event
16-
17-
In order to react to an event, you first need to _subscribe_ to it.
18-
19-
To subscribe to an event, register an _event listener_ in the service's `Init` method:
20+
The return values is more flexible, and may consist of none, one, or two
21+
values. If there are two values returned, the second value should be
22+
either an `bool` or `error` indicating the success or failure of the
23+
call. The first value `U` carries a value of any exported type that
24+
makes sense for the service.
2025

21-
```go
22-
func (s *MyService) Init() error {
23-
s.bus.AddEventListener(s.UserCreated)
24-
return nil
25-
}
26+
Following is an example of an interface providing method signatures for
27+
some calls adhering to these guidelines:
2628

27-
func (s *MyService) UserCreated(event *events.UserCreated) error {
28-
// ...
29+
```
30+
type Alphabetical interface {
31+
// GetLetter returns either an error or letter.
32+
GetLetter(context.Context, GetLetterQuery) (Letter, error)
33+
// ListCachedLetters cannot fail, and doesn't return an error.
34+
ListCachedLetters(context.Context, ListCachedLettersQuery) Letters
35+
// DeleteLetter doesn't have any return values other than errors, so it
36+
// returns only an error.
37+
DeleteLetter(context.Contxt, DeleteLetterCommand) error
2938
}
3039
```
3140

32-
**Tip:** Browse the available events in the `events` package.
41+
> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, `GetDashboardQuery` and `DeletePlaylistCommand`.
3342
34-
### Publish an event
43+
The use of complex types for arguments in Go means a few different
44+
things for us, it provides us with the equivalent of named parameters
45+
from other languages, and it reduces the headache of figuring out which
46+
argument is which that often occurs with three or more arguments.
3547

36-
If you want to let other parts of the application react to changes in a service, you can publish your own events:
48+
On the flip-side, it means that all input parameters are optional and
49+
that it is up to the programmer to make sure that the zero value is
50+
useful or at least safe for all fields and that while it's easy to add
51+
another field, if that field must be set for the correct function of the
52+
service that is not detectable at compile time.
3753

38-
```go
39-
event := &events.StickersSentEvent {
40-
UserID: "taylor",
41-
Count: 1,
42-
}
43-
if err := s.bus.Publish(event); err != nil {
44-
return err
45-
}
46-
```
54+
### Queries with Result fields
4755

48-
## Commands
56+
Some queries have a Result field that is mutated and populated by the
57+
method being called. This is a remainder from when the _bus_ was used
58+
for sending commands and queries as well as for events.
4959

50-
A command is a request for an action to be taken. Unlike an event's fire-and-forget approach, a command can fail as it is handled. The handler will then return an error.
60+
All bus commands and queries had to implement the Go type
61+
`func(ctx context.Context, msg interface{}) error`
62+
and mutation of the `msg` variable or returning structured information in
63+
`error` were the two most convenient ways to communicate with the caller.
5164

52-
> Because we request an operation to be performed, command are written in imperative mood, such as `CreateFolderCommand`, and `DeletePlaylistCommand`.
65+
All `Result` fields should be refactored so that they are returned from
66+
the query method:
5367

54-
### Dispatch a command
55-
56-
To dispatch a command, pass the `context.Context` and object to the `DispatchCtx` method:
68+
```
69+
type GetQuery struct {
70+
Something int
5771
58-
```go
59-
// context.Context from caller
60-
ctx := req.Request.Context()
61-
cmd := &models.SendStickersCommand {
62-
UserID: "taylor",
63-
Count: 1,
72+
Result ResultType
6473
}
65-
if err := s.bus.DispatchCtx(ctx, cmd); err != nil {
66-
if err == bus.ErrHandlerNotFound {
67-
return nil
68-
}
69-
return err
74+
75+
func (s *Service) Get(ctx context.Context, cmd *GetQuery) error {
76+
// ...do something
77+
cmd.Result = result
78+
return nil
7079
}
7180
```
7281

73-
> **Note:** `DispatchCtx` will return an error if no handler is registered for that command.
82+
should become
7483

75-
> **Note:** `Dispatch` currently exists and requires no `context.Context` to be provided, but it's strongly suggested to not use this since there's an ongoing refactoring to remove usage of non-context-aware functions/methods and use context.Context everywhere.
76-
77-
**Tip:** Browse the available commands in the `models` package.
78-
79-
### Handle commands
80-
81-
Let other parts of the application dispatch commands to a service, by registering a _command handler_:
82-
83-
To handle a command, register a command handler in the `Init` function.
84-
85-
```go
86-
func (s *MyService) Init() error {
87-
s.bus.AddHandlerCtx(s.SendStickers)
88-
return nil
84+
```
85+
type GetQuery struct {
86+
Something int
8987
}
9088
91-
func (s *MyService) SendStickers(ctx context.Context, cmd *models.SendStickersCommand) error {
92-
// ...
89+
func (s *Service) Get(ctx context.Context, cmd GetQuery) (ResultType, error) {
90+
// ...do something
91+
return result, nil
9392
}
9493
```
9594

96-
> **Note:** The handler method may return an error if unable to complete the command.
95+
## Events
9796

98-
> **Note:** `AddHandler` currently exists and requires no `context.Context` to be provided, but it's strongly suggested to not use this since there's an ongoing refactoring to remove usage of non-context-aware functions/methods and use context.Context everywhere.
97+
An event is something that happened in the past. Since an event has already happened, you can't change it. Instead, you can react to events by triggering additional application logic to be run, whenever they occur.
9998

100-
## Queries
99+
> Because they happened in the past, event names are written in past tense, such as `UserCreated`, and `OrgUpdated`.
101100
102-
A command handler can optionally populate the command sent to it. This pattern is commonly used to implement _queries_.
101+
### Subscribe to an event
103102

104-
### Making a query
103+
In order to react to an event, you first need to _subscribe_ to it.
105104

106-
To make a query, dispatch the query instance just like you would a command. When the `DispatchCtx` method returns, the `Results` field contains the result of the query.
105+
To subscribe to an event, register an _event listener_ in the service's `Init` method:
107106

108107
```go
109-
// context.Context from caller
110-
ctx := req.Request.Context()
111-
query := &models.FindDashboardQuery{
112-
ID: "foo",
113-
}
114-
if err := bus.Dispatch(ctx, query); err != nil {
115-
return err
108+
func (s *MyService) Init() error {
109+
s.bus.AddEventListener(s.UserCreated)
110+
return nil
116111
}
117-
// The query now contains a result.
118-
for _, item := range query.Results {
112+
113+
func (s *MyService) UserCreated(event *events.UserCreated) error {
119114
// ...
120115
}
121116
```
122117

123-
> **Note:** `Dispatch` currently exists and requires no `context.Context` to be provided, but it's strongly suggested to not use this since there's an ongoing refactoring to remove usage of non-context-aware functions/methods and use context.Context everywhere.
118+
**Tip:** Browse the available events in the `events` package.
124119

125-
### Return query results
120+
### Publish an event
126121

127-
To return results for a query, set any of the fields on the query argument before returning:
122+
If you want to let other parts of the application react to changes in a service, you can publish your own events:
128123

129124
```go
130-
func (s *MyService) FindDashboard(ctx context.Context, query *models.FindDashboardQuery) error {
131-
// ...
132-
query.Result = dashboard
133-
return nil
125+
event := &events.StickersSentEvent {
126+
UserID: "taylor",
127+
Count: 1,
128+
}
129+
if err := s.bus.Publish(event); err != nil {
130+
return err
134131
}
135132
```
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Errors
2+
3+
Grafana introduced its own error type `github.com/grafana/grafana/pkg/util/errutil.Error`
4+
in June 2022. It's built on top of the Go `error` interface extended to
5+
contain all the information necessary by Grafana to handle errors in an
6+
informative and safe way.
7+
8+
Previously, Grafana has passed around regular Go errors and have had to
9+
rely on bespoke solutions in API handlers to communicate informative
10+
messages to the end-user. With the new `errutil.Error`, the API handlers
11+
can be slimmed as information about public messaging, structured data
12+
related to the error, localization metadata, log level, HTTP status
13+
code, and so forth are carried by the error.
14+
15+
## Basic use
16+
17+
### Declaring errors
18+
19+
For a service, declare the different categories of errors that may occur
20+
from your service (this corresponds to what you might want to have
21+
specific public error messages or their templates for) by globally
22+
constructing variables using the `errutil.NewBase(status, messageID, opts...)`
23+
function.
24+
25+
The status code loosely corresponds to HTTP status codes and provides a
26+
default log level for errors to ensure that the request logging is
27+
properly informing administrators about various errors occurring in
28+
Grafana (e.g. `StatusBadRequest` is generally speaking not as relevant
29+
as `StatusInternal`). All available status codes live in the `errutil`
30+
package and have names starting with `Status`.
31+
32+
The messageID is constructed as `<servicename>.<error-identifier>` where
33+
the `<servicename>` corresponds to the root service directory per
34+
[the package hierarchy](package-hierarchy.md) and `<error-identifier>`
35+
is a short identifier using dashes for word separation that identifies
36+
the specific category of errors within the service.
37+
38+
To set a static message sent to the client when the error occurs, the
39+
`errutil.WithPublicMessage(message string)` option may be appended to
40+
the NewBase function call. For dynamic messages or more options, refer
41+
to the `errutil` package's GoDocs.
42+
43+
Errors are then constructed using the `Base.Errorf` method, which
44+
functions like the [fmt.Errorf](https://pkg.go.dev/fmt#Errorf) method
45+
except that it creates an `errutil.Error`.
46+
47+
```go
48+
package main
49+
50+
import (
51+
"errors"
52+
"github.com/grafana/grafana/pkg/util/errutil"
53+
"example.org/thing"
54+
)
55+
56+
var ErrBaseNotFound = errutil.NewBase(errutil.StatusNotFound, "main.not-found", errutil.WithPublicMessage("Thing not found"))
57+
58+
func Look(id int) (*Thing, error) {
59+
t, err := thing.GetByID(id)
60+
if errors.Is(err, thing.ErrNotFound) {
61+
return nil, ErrBaseNotFound.Errorf("did not find thing with ID %d: %w", id, err)
62+
}
63+
64+
return t, nil
65+
}
66+
```
67+
68+
Check out [errutil's GoDocs](https://pkg.go.dev/github.com/grafana/[email protected]/pkg/util/errutil)
69+
for details on how to construct and use Grafana style errors.
70+
71+
### Handling errors in the API
72+
73+
API handlers use the `github.com/grafana/grafana/pkg/api/response.Err`
74+
function to create responses based on `errutil.Error`s.
75+
76+
> **Note:** (@sakjur 2022-06) `response.Err` requires all errors to be
77+
> `errutil.Error` or it'll be considered an internal server error.
78+
> This is something that should be fixed in the near future to allow
79+
> fallback behavior to make it possible to correctly handle Grafana
80+
> style errors if they're present but allow fallback to a reasonable
81+
> default otherwise.

0 commit comments

Comments
 (0)