Skip to content

Commit

Permalink
Grafana datasource exchange (#207)
Browse files Browse the repository at this point in the history
* ds exchange draft

* json schemas

* sorting

* json schemas

* better example

* fix name

* remove negative tests

* simplify test
  • Loading branch information
PietroPasotti authored Dec 4, 2024
1 parent 2556a56 commit 539fa56
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 1 deletion.
78 changes: 78 additions & 0 deletions docs/json_schemas/grafana_datasource_exchange/v0/provider.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$defs": {
"BaseModel": {
"properties": {},
"title": "BaseModel",
"type": "object"
},
"GrafanaDatasource": {
"properties": {
"type": {
"description": "Type of the datasource, typically one of https://grafana.com/docs/grafana/latest/datasources/#built-in-core-data-sources.",
"examples": [
"tempo",
"loki",
"prometheus",
"elasticsearch"
],
"title": "Type",
"type": "string"
},
"uid": {
"description": "Grafana datasource UID, as assigned by Grafana.",
"title": "Uid",
"type": "string"
}
},
"required": [
"type",
"uid"
],
"title": "GrafanaDatasource",
"type": "object"
},
"GrafanaSourceAppData": {
"description": "Application databag model for the requirer side of this interface.",
"properties": {
"datasources": {
"contentMediaType": "application/json",
"contentSchema": {
"items": {
"$ref": "#/$defs/GrafanaDatasource"
},
"type": "array"
},
"title": "Datasources",
"type": "string"
}
},
"required": [
"datasources"
],
"title": "GrafanaSourceAppData",
"type": "object"
}
},
"description": "The schemas for the requirer side of this interface.",
"properties": {
"unit": {
"anyOf": [
{
"$ref": "#/$defs/BaseModel"
},
{
"type": "null"
}
],
"default": null
},
"app": {
"$ref": "#/$defs/GrafanaSourceAppData"
}
},
"required": [
"app"
],
"title": "ProviderSchema",
"type": "object"
}
78 changes: 78 additions & 0 deletions docs/json_schemas/grafana_datasource_exchange/v0/requirer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$defs": {
"BaseModel": {
"properties": {},
"title": "BaseModel",
"type": "object"
},
"GrafanaDatasource": {
"properties": {
"type": {
"description": "Type of the datasource, typically one of https://grafana.com/docs/grafana/latest/datasources/#built-in-core-data-sources.",
"examples": [
"tempo",
"loki",
"prometheus",
"elasticsearch"
],
"title": "Type",
"type": "string"
},
"uid": {
"description": "Grafana datasource UID, as assigned by Grafana.",
"title": "Uid",
"type": "string"
}
},
"required": [
"type",
"uid"
],
"title": "GrafanaDatasource",
"type": "object"
},
"GrafanaSourceAppData": {
"description": "Application databag model for the requirer side of this interface.",
"properties": {
"datasources": {
"contentMediaType": "application/json",
"contentSchema": {
"items": {
"$ref": "#/$defs/GrafanaDatasource"
},
"type": "array"
},
"title": "Datasources",
"type": "string"
}
},
"required": [
"datasources"
],
"title": "GrafanaSourceAppData",
"type": "object"
}
},
"description": "The schemas for the provider side of this interface.",
"properties": {
"unit": {
"anyOf": [
{
"$ref": "#/$defs/BaseModel"
},
{
"type": "null"
}
],
"default": null
},
"app": {
"$ref": "#/$defs/GrafanaSourceAppData"
}
},
"required": [
"app"
],
"title": "RequirerSchema",
"type": "object"
}
79 changes: 79 additions & 0 deletions interfaces/grafana_datasource_exchange/v0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# `grafana_datasource_exchange`

The `grafana_datasource_exchange` interface allows charms that generate telemetry and have a reference to the datasources where said telemetry is queriable,
to share those references to other charms for correlation and cross-referencing purposes.

## Usage

This document describes the expected behavior of any charm exposing a `grafana_datasource_exchange` endpoint.

The reference implementation for this interface is implemented in the [`cosl`](https://github.com/canonical/cos-lib) library.
Charm developers are free to provide alternative libraries as long as they fulfill the behavioral and schematic requirements described in this document.

## Direction
The `grafana_datasource_exchange` interface implements a symmetrical provider/requirer pattern.
Symmetrical, in the sense that the role doesn't matter and the data to be exchanged is the same for the requirer and the provider.

```mermaid
flowchart TD
Provider -- Datasources --> Requirer
Requirer -- Datasources --> Provider
```

## Behavior

The requirer and the provider need to adhere to a certain set of criteria to be considered compatible with the interface.

### Provider & Requirer

- Is expected to expose a server implementing [the grafana source HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/data_source/). In other words, it's expected to expose one or more valid grafana datasources.
- Is expected to register each datasource endpoint (one per unit) with a central grafana application and obtain a Datasource UID for each one of them.
- Is expected to share via application data, as a json-encoded array (sorted by UID), the following information:
- for each datasource (which technically will likely mean, for each unit of the application):
- the datasource UID: an arbitrary string, that is expected to be unique for the grafana instance
- the datasource type: a grafana datasource type name (typically will be [one of the built-in ones](https://grafana.com/docs/grafana/latest/datasources/#built-in-core-data-sources))

To avoid complexity, we stipulate that the data will be provided in bulk: only 'fully specified' datasources will be shared, i.e. this is not a valid databag state:

```yaml
application_data: {
datasources:
[
{
type: tempo,
},
]
}
```

In other words, if the application still has to hear back from Grafana what the UID of the datasource is, it should **not** add it to its `grafana_datasource_exchange` endpoints.
Only when it knows what UID is assigned to a datasource, then will it add the datasource to this relation.


## Relation Data

[\[Pydantic model\]](./schema.py)

#### Example

```yaml
application_data: {
datasources:
[
{
type: tempo,
uid: 0000-0000-0000-0001
},
{
type: prometheus,
uid: 0000-0000-0000-0002
},
]
}
```

#### Notes

- Since this interface is symmetrical, each application will likely have to implement both a requirer and a provider endpoint for it, to avoid having strange constraints on the integration topology.
- The data that is being exchanged comes in part from the application themselves (the datasource type), but in part from another integration (Grafana assigns the UIDs and communicates them back via the `grafana_datasource` interface). Since we cannot assume which integration is created first and what the event sequence will look like, this interface cannot commit to a specific event by which the data should be written. Instead, the only guarantee an implementer should give, is that _eventually_ the data will be provided.
- The 'sorting by UID' feature is required to prevent the databag hash to keep flapping and trigger endless cascades of relation-changed events.
26 changes: 26 additions & 0 deletions interfaces/grafana_datasource_exchange/v0/interface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: grafana_datasource_exchange

internal: true

version: 0

status: draft

providers:
- name: tempo-coordinator-k8s
url: https://github.com/canonical/tempo-coordinator-k8s-operator
branch: datasource_exchange
test_setup:
location: tests/interface/conftest.py
identifier: grafana_datasource_exchange_tester

requirers:
- name: tempo-coordinator-k8s
url: https://github.com/canonical/tempo-coordinator-k8s-operator
branch: datasource_exchange
test_setup:
location: tests/interface/conftest.py
identifier: grafana_datasource_exchange_tester


maintainer: observability
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import json

from interface_tester import Tester
from scenario import State, Relation


def test_datasource_exchange():
# GIVEN the grafana_datasource interface has shared one or more source UIDs
source_exchange = Relation(
endpoint='grafana-source-exchange',
interface='grafana_datasource_exchange',
remote_app_name='bar'
)
tester = Tester(state_in=State(
relations=[
source_exchange
]
))
# WHEN the provider processes any relation event
tester.run('grafana-source-exchange-relation-changed')
# THEN the provider publishes valid data
tester.assert_schema_valid()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# given that this interface is symmetric, and we expect each provider
# to also be a requirer, we omit the requirer tests.
26 changes: 26 additions & 0 deletions interfaces/grafana_datasource_exchange/v0/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import List

from interface_tester.schema_base import DataBagSchema
from pydantic import Json, BaseModel, Field


class GrafanaDatasource(BaseModel):
type: str = Field(description="Type of the datasource, typically one of "
"https://grafana.com/docs/grafana/latest/datasources/#built-in-core-data-sources.",
examples=["tempo", "loki", "prometheus", "elasticsearch"])
uid: str = Field(description="Grafana datasource UID, as assigned by Grafana.")


class GrafanaSourceAppData(BaseModel):
"""Application databag model for the requirer side of this interface."""
datasources: Json[List[GrafanaDatasource]]


class ProviderSchema(DataBagSchema):
"""The schemas for the requirer side of this interface."""
app: GrafanaSourceAppData


class RequirerSchema(DataBagSchema):
"""The schemas for the provider side of this interface."""
app: GrafanaSourceAppData
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ interface_tests = [
"pytest>=7.3.1",
"ops-scenario>=7.0.5",
"requests==2.28.1",
"virtualenv==20.25.1"
"virtualenv==20.25.1",
]

[project.urls]
Expand Down

0 comments on commit 539fa56

Please sign in to comment.