mirror of https://github.com/grafana/grafana
Docs: Update backend architecture contributor documentation (#51172)
Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>pull/51650/head
parent
d63ffa314e
commit
0e7a495829
@ -0,0 +1,81 @@ |
|||||||
|
# Errors |
||||||
|
|
||||||
|
Grafana introduced its own error type `github.com/grafana/grafana/pkg/util/errutil.Error` |
||||||
|
in June 2022. It's built on top of the Go `error` interface extended to |
||||||
|
contain all the information necessary by Grafana to handle errors in an |
||||||
|
informative and safe way. |
||||||
|
|
||||||
|
Previously, Grafana has passed around regular Go errors and have had to |
||||||
|
rely on bespoke solutions in API handlers to communicate informative |
||||||
|
messages to the end-user. With the new `errutil.Error`, the API handlers |
||||||
|
can be slimmed as information about public messaging, structured data |
||||||
|
related to the error, localization metadata, log level, HTTP status |
||||||
|
code, and so forth are carried by the error. |
||||||
|
|
||||||
|
## Basic use |
||||||
|
|
||||||
|
### Declaring errors |
||||||
|
|
||||||
|
For a service, declare the different categories of errors that may occur |
||||||
|
from your service (this corresponds to what you might want to have |
||||||
|
specific public error messages or their templates for) by globally |
||||||
|
constructing variables using the `errutil.NewBase(status, messageID, opts...)` |
||||||
|
function. |
||||||
|
|
||||||
|
The status code loosely corresponds to HTTP status codes and provides a |
||||||
|
default log level for errors to ensure that the request logging is |
||||||
|
properly informing administrators about various errors occurring in |
||||||
|
Grafana (e.g. `StatusBadRequest` is generally speaking not as relevant |
||||||
|
as `StatusInternal`). All available status codes live in the `errutil` |
||||||
|
package and have names starting with `Status`. |
||||||
|
|
||||||
|
The messageID is constructed as `<servicename>.<error-identifier>` where |
||||||
|
the `<servicename>` corresponds to the root service directory per |
||||||
|
[the package hierarchy](package-hierarchy.md) and `<error-identifier>` |
||||||
|
is a short identifier using dashes for word separation that identifies |
||||||
|
the specific category of errors within the service. |
||||||
|
|
||||||
|
To set a static message sent to the client when the error occurs, the |
||||||
|
`errutil.WithPublicMessage(message string)` option may be appended to |
||||||
|
the NewBase function call. For dynamic messages or more options, refer |
||||||
|
to the `errutil` package's GoDocs. |
||||||
|
|
||||||
|
Errors are then constructed using the `Base.Errorf` method, which |
||||||
|
functions like the [fmt.Errorf](https://pkg.go.dev/fmt#Errorf) method |
||||||
|
except that it creates an `errutil.Error`. |
||||||
|
|
||||||
|
```go |
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"github.com/grafana/grafana/pkg/util/errutil" |
||||||
|
"example.org/thing" |
||||||
|
) |
||||||
|
|
||||||
|
var ErrBaseNotFound = errutil.NewBase(errutil.StatusNotFound, "main.not-found", errutil.WithPublicMessage("Thing not found")) |
||||||
|
|
||||||
|
func Look(id int) (*Thing, error) { |
||||||
|
t, err := thing.GetByID(id) |
||||||
|
if errors.Is(err, thing.ErrNotFound) { |
||||||
|
return nil, ErrBaseNotFound.Errorf("did not find thing with ID %d: %w", id, err) |
||||||
|
} |
||||||
|
|
||||||
|
return t, nil |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Check out [errutil's GoDocs](https://pkg.go.dev/github.com/grafana/grafana@v0.0.0-20220621133844-0f4fc1290421/pkg/util/errutil) |
||||||
|
for details on how to construct and use Grafana style errors. |
||||||
|
|
||||||
|
### Handling errors in the API |
||||||
|
|
||||||
|
API handlers use the `github.com/grafana/grafana/pkg/api/response.Err` |
||||||
|
function to create responses based on `errutil.Error`s. |
||||||
|
|
||||||
|
> **Note:** (@sakjur 2022-06) `response.Err` requires all errors to be |
||||||
|
> `errutil.Error` or it'll be considered an internal server error. |
||||||
|
> This is something that should be fixed in the near future to allow |
||||||
|
> fallback behavior to make it possible to correctly handle Grafana |
||||||
|
> style errors if they're present but allow fallback to a reasonable |
||||||
|
> default otherwise. |
@ -1,16 +1,217 @@ |
|||||||
# Package hierarchy |
# Package hierarchy |
||||||
|
|
||||||
The Go package hierarchy in Grafana should be organized logically (Ben Johnson's |
The Go packages in Grafana should be packaged by feature, keeping |
||||||
[article](https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1) served as inspiration), according to the |
packages as small as reasonable while retaining a clear sole ownership |
||||||
following principles: |
of a single domain. |
||||||
|
|
||||||
- Domain types and interfaces should be in "root" packages (not necessarily at the very top, of the hierarchy, but |
[Ben Johnson's standard package layout](https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1) serves as |
||||||
logical roots) |
inspiration for the way we organize packages. |
||||||
- Sub-packages should depend on roots - sub-packages here typically contain implementations, for example of services |
|
||||||
|
## Principles of how to structure a service in Grafana |
||||||
|
|
||||||
|
[](services.md) |
||||||
|
|
||||||
|
### Domain types and interfaces should be in local "root" packages |
||||||
|
|
||||||
|
Let's say you're creating a _tea pot_ service, place everything another |
||||||
|
service needs to interact with the tea pot service in |
||||||
|
_pkg/services/teapot_, choosing a name according to |
||||||
|
[Go's package naming conventions](https://go.dev/blog/package-names). |
||||||
|
|
||||||
|
Typically, you'd have one or more interfaces that your service provides |
||||||
|
in the root package along with any types, errors, and other constants |
||||||
|
that makes sense for another service interacting with this service to |
||||||
|
use. |
||||||
|
|
||||||
|
Avoid depending on other services when structuring the root package to |
||||||
|
reduce the risk of running into circular dependencies. |
||||||
|
|
||||||
|
### Sub-packages should depend on roots, not the other way around |
||||||
|
|
||||||
|
Small-to-medium sized packages should be able to have only a single |
||||||
|
sub-package containing the implementation of the service. By moving the |
||||||
|
implementation into a separate package we reduce the risk of triggering |
||||||
|
circular dependencies (in Go, circular dependencies are evaluated per |
||||||
|
package and this structure logically moves it to be per type or function |
||||||
|
declaration). |
||||||
|
|
||||||
|
Large packages may need utilize multiple sub-packages at the discretion |
||||||
|
of the implementor. Keep interfaces and domain types to the root |
||||||
|
package. |
||||||
|
|
||||||
|
### Try to name sub-packages for project wide uniqueness |
||||||
|
|
||||||
|
Prefix sub-packages with the service name or an abbreviation of the |
||||||
|
service name (whichever is more appropriate) to provide an ideally |
||||||
|
unique package name. This allows `teaimpl` to be distinguished from |
||||||
|
`coffeeimpl` without the need for package aliases, and encourages the |
||||||
|
use of the same name to reference your package throughout the codebase. |
||||||
|
|
||||||
|
### A well-behaving service provides test doubles for itself |
||||||
|
|
||||||
|
Other services may depend on your service, and it's good practice to |
||||||
|
provide means for those services to set up a test instance of the |
||||||
|
dependency as needed. Refer to |
||||||
|
[Google Testing's Testing on the Toilet: Know Your Test Doubles](https://testing.googleblog.com/2013/07/testing-on-toilet-know-your-test-doubles.html) for a brief |
||||||
|
explanation of how we semantically aim to differentiate fakes, mocks, |
||||||
|
and stubs within our codebase. |
||||||
|
|
||||||
|
Place test doubles in a sub-package to your root package named |
||||||
|
`<servicename>test` or `<service-abbreviation>test`, such that the `teapot` service may have the |
||||||
|
`teapottest` or `teatest` |
||||||
|
|
||||||
|
A stub or mock may be sufficient if the service is not a dependency of a |
||||||
|
lot of services or if it's called primarily for side effects so that a |
||||||
|
no-op default behavior makes sense. |
||||||
|
|
||||||
|
Services which serve many other services and where it's feasible should |
||||||
|
provide an in-memory backed test fake that can be used like the |
||||||
|
regular service without the need of complicated setup. |
||||||
|
|
||||||
|
### Separate store and logic |
||||||
|
|
||||||
|
When building a new service, data validation, manipulation, scheduled |
||||||
|
events and so forth should be collected in a service implementation that |
||||||
|
is built to be agnostic about its store. |
||||||
|
|
||||||
|
The storage should be an interface that is not directly called from |
||||||
|
outside the service and should be kept to a minimum complexity to |
||||||
|
provide the functionality necessary for the service. |
||||||
|
|
||||||
|
A litmus test to reduce the complexity of the storage interface is |
||||||
|
whether an in-memory implementation is a feasible test double to build |
||||||
|
to test the service. |
||||||
|
|
||||||
|
### Outside the service root |
||||||
|
|
||||||
|
Some parts of the service definition remains outside the |
||||||
|
service directory and reflects the legacy package hierarchy. |
||||||
|
As of June 2022, the parts that remain outside the service are: |
||||||
|
|
||||||
|
#### Migrations |
||||||
|
|
||||||
|
`pkg/services/sqlstore/migrations` contains all migrations for SQL |
||||||
|
databases, for all services (not including Grafana Enterprise). |
||||||
|
Migrations are written per the [database.md](database.md#migrations) document. |
||||||
|
|
||||||
|
#### API endpoints |
||||||
|
|
||||||
|
`pkg/api/api.go` contains the endpoint definitions for the most of |
||||||
|
Grafana HTTP API (not including Grafana Enterprise). |
||||||
|
|
||||||
## Practical example |
## Practical example |
||||||
|
|
||||||
The `pkg/plugins` package contains plugin domain types, for example `DataPlugin`, and also interfaces |
The following is a simplified example of the package structure for a |
||||||
such as `RequestHandler`. Then you have the `pkg/plugins/managers` subpackage, which contains concrete implementations |
service that doesn't do anything in particular. |
||||||
such as the service `PluginManager`. The subpackage `pkg/plugins/backendplugin/coreplugin` contains `plugins.DataPlugin` |
|
||||||
implementations. |
None of the methods or functions are populated and in practice most |
||||||
|
packages will consist of multiple files. There isn't a Grafana-wide |
||||||
|
convention for which files should exist and contain what. |
||||||
|
|
||||||
|
`pkg/services/alphabetical` |
||||||
|
|
||||||
|
``` |
||||||
|
package alphabetical |
||||||
|
|
||||||
|
type Alphabetical interface { |
||||||
|
// GetLetter returns either an error or letter. |
||||||
|
GetLetter(context.Context, GetLetterQuery) (Letter, error) |
||||||
|
// ListCachedLetters cannot fail, and doesn't return an error. |
||||||
|
ListCachedLetters(context.Context, ListCachedLettersQuery) Letters |
||||||
|
// DeleteLetter doesn't have any return values other than errors, so it |
||||||
|
// returns only an error. |
||||||
|
DeleteLetter(context.Contxt, DeltaCommand) error |
||||||
|
} |
||||||
|
|
||||||
|
type Letter byte |
||||||
|
|
||||||
|
type Letters []Letter |
||||||
|
|
||||||
|
type GetLetterQuery struct { |
||||||
|
ID int |
||||||
|
} |
||||||
|
|
||||||
|
// Create queries/commands for methods even if they are empty. |
||||||
|
type ListCachedLettersQuery struct {} |
||||||
|
|
||||||
|
type DeleteLetterCommand struct { |
||||||
|
ID int |
||||||
|
} |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
`pkg/services/alphabetical/alphabeticalimpl` |
||||||
|
|
||||||
|
``` |
||||||
|
package alphabeticalimpl |
||||||
|
|
||||||
|
// this name can be whatever, it's not supposed to be used from outside |
||||||
|
// the service except for in Wire. |
||||||
|
type Svc struct { … } |
||||||
|
|
||||||
|
func ProviceSvc(numbers numerical.Numerical, db db.DB) Svc { … } |
||||||
|
|
||||||
|
func (s *Svc) GetLetter(ctx context.Context, q root.GetLetterQuery) (root.Letter, error) { … } |
||||||
|
func (s *Svc) ListCachedLetters(ctx context.Context, q root.ListCachedLettersQuery) root.Letters { … } |
||||||
|
func (s *Svc) DeleteLetter(ctx context.Context, q root.DeleteLetterCommand) error { … } |
||||||
|
|
||||||
|
type letterStore interface { |
||||||
|
Get(ctx.Context, id int) (root.Letter, error) |
||||||
|
Delete(ctx.Context, root.DeleteLetterCommand) error |
||||||
|
} |
||||||
|
|
||||||
|
type sqlLetterStore struct { |
||||||
|
db.DB |
||||||
|
} |
||||||
|
|
||||||
|
func (s *sqlStore) Get(ctx.Context, id int) (root.Letter, error) { … } |
||||||
|
func (s *sqlStore) Delete(ctx.Context, root.DeleteLetterCommand) error { … } |
||||||
|
``` |
||||||
|
|
||||||
|
## Legacy package hierarchy |
||||||
|
|
||||||
|
> **Note:** A lot of services still adhere to the legacy model as outlined below. While it is ok to |
||||||
|
> extend existing services based on the legacy model, you are _strongly_ encouraged to structure any |
||||||
|
> new services or major refactorings using the new package layout. |
||||||
|
|
||||||
|
Grafana has long used a package-by-layer layout where domain types |
||||||
|
are placed in **pkg/models**, all SQL logic in **pkg/services/sqlstore**, |
||||||
|
and so forth. |
||||||
|
|
||||||
|
This is an example of how the _tea pot_ service could be structured |
||||||
|
throughout the codebase in the legacy model. |
||||||
|
|
||||||
|
- _pkg/_ |
||||||
|
- _api/_ |
||||||
|
- _api.go_ contains the endpoints for the |
||||||
|
- _tea_pot.go_ contains methods on the _pkg/api.HTTPServer_ type |
||||||
|
that interacts with the service based on queries coming in via the HTTP |
||||||
|
API. |
||||||
|
- _dtos/tea_pot.go_ extends the _pkg/models_ file with types |
||||||
|
that are meant for translation to and from the API. It's not as commonly |
||||||
|
present as _pkg/models_. |
||||||
|
- _models/tea_pot.go_ contains the models for the service, this |
||||||
|
includes the _command_ and _query_ structs that are used when calling |
||||||
|
the service or SQL store methods related to the service and also any |
||||||
|
models representing an abstraction provided by the service. |
||||||
|
- _services/_ |
||||||
|
- _sqlstore_ |
||||||
|
- _tea_pot.go_ contains SQL queries for |
||||||
|
interacting with stored objects related to the tea pot service. |
||||||
|
- _migrations/tea_pot.go_ contains the migrations necessary to |
||||||
|
build the |
||||||
|
- _teapot/\*_ contains functions or a service for doing |
||||||
|
logical operations beyond those done in _pkg/api_ or _pkg/services/sqlstore_ |
||||||
|
for the service. |
||||||
|
|
||||||
|
The implementation of legacy services varies widely from service to |
||||||
|
service, some or more of these files may be missing and there may be |
||||||
|
more files related to a service than those listed here. |
||||||
|
|
||||||
|
Some legacy services providing infrastructure will also take care of the |
||||||
|
integration with several domains. The cleanup service both |
||||||
|
provides the infrastructure to occasionally run cleanup scripts and |
||||||
|
defines the cleanup scripts. Ideally, this would be migrated |
||||||
|
to only handle the scheduling and synchronization of clean up jobs. |
||||||
|
The logic for the individual jobs would be placed with a service that is |
||||||
|
related to whatever is being cleaned up. |
||||||
|
Loading…
Reference in new issue