Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit 9d63b8e

Browse files
feat(client): add support for metrics (#831)
## Change Summary closes #434 TODO: - [x] support globalLabels - [x] check if int / float is right - [x] test multiple client instances - [x] document ## Checklist - [ ] Unit tests for the changes exist - [ ] Tests pass without significant drop in coverage - [ ] Documentation reflects changes where applicable - [ ] Test snapshots have been [updated](https://prisma-client-py.readthedocs.io/en/latest/contributing/contributing/#snapshot-tests) if applicable ## Agreement By submitting this pull request, I confirm that you can use, modify, copy and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent a2ec5f8 commit 9d63b8e

23 files changed

+620
-28
lines changed

databases/sync_tests/test_metrics.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from prisma import Prisma
2+
from prisma._compat import model_json
3+
4+
5+
def test_prometheus(client: Prisma) -> None:
6+
"""Metrics can be returned in Prometheus format"""
7+
client.user.create(data={'name': 'Robert'})
8+
9+
metrics = client.get_metrics(format='prometheus')
10+
11+
assert 'prisma_client_queries_total' in metrics
12+
assert 'prisma_datasource_queries_total' in metrics
13+
assert 'prisma_client_queries_active' in metrics
14+
assert 'prisma_client_queries_duration_histogram_ms_bucket' in metrics
15+
16+
17+
def test_json_string(client: Prisma) -> None:
18+
"""Metrics can be serlialized to JSON"""
19+
client.user.create(data={'name': 'Robert'})
20+
21+
metrics = client.get_metrics()
22+
assert isinstance(model_json(metrics), str)
23+
24+
25+
def test_json(client: Prisma) -> None:
26+
"""Metrics returned in the JSON format"""
27+
client.user.create(data={'name': 'Robert'})
28+
29+
metrics = client.get_metrics(format='json')
30+
31+
assert len(metrics.counters) > 0
32+
assert metrics.counters[0].value > 0
33+
34+
assert len(metrics.gauges) > 0
35+
gauge = next(
36+
filter(
37+
lambda g: g.key == 'prisma_pool_connections_open', metrics.gauges
38+
)
39+
)
40+
assert gauge.value > 0
41+
42+
assert len(metrics.histograms) > 0
43+
assert metrics.histograms[0].value.sum > 0
44+
assert metrics.histograms[0].value.count > 0
45+
46+
assert len(metrics.histograms[0].value.buckets) > 0
47+
48+
for bucket in metrics.histograms[0].value.buckets:
49+
assert bucket.max_value >= 0
50+
assert bucket.total_count >= 0
51+
52+
53+
def test_global_labels(client: Prisma) -> None:
54+
metrics = client.get_metrics(global_labels={'foo': 'bar'})
55+
assert metrics.counters[0].labels == {'foo': 'bar'}

databases/templates/schema.prisma.jinja2

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,20 @@ model Profile {
3939
}
4040

4141
model Post {
42-
id String @id @default(uuid())
43-
created_at DateTime @default(now())
44-
updated_at DateTime @updatedAt
42+
id String @id @default(uuid())
43+
created_at DateTime @default(now())
44+
updated_at DateTime @updatedAt
4545
title String
4646
description String?
47-
published Boolean @default(false)
48-
views Int @default(0)
49-
author User? @relation(fields: [author_id], references: [id])
47+
published Boolean @default(false)
48+
views Int @default(0)
49+
author User? @relation(fields: [author_id], references: [id])
5050
author_id String?
51-
categories Category[]
51+
categories Category[]
5252
}
5353

5454
model Category {
55-
id String @id @default(uuid())
55+
id String @id @default(uuid())
5656
posts Post[]
5757
name String
5858
}
@@ -80,10 +80,10 @@ model Types {
8080

8181
// TODO: optional for these too
8282
{% if config.supports_feature('enum') %}
83-
enum Role @default(USER)
83+
enum Role @default(USER)
8484
{% endif %}
8585
{% if config.supports_feature('json') %}
86-
json_obj Json? @default("{}")
86+
json_obj Json? @default("{}")
8787
{% endif %}
8888
}
8989

@@ -123,6 +123,7 @@ model ListsDefaults {
123123
roles Role[] @default([USER])
124124
{% endif %}
125125
}
126+
126127
{% endif %}
127128

128129
// these models are here for testing different combinations of unique constraints
@@ -173,6 +174,7 @@ model Unique6 {
173174

174175
@@unique([name, role])
175176
}
177+
176178
{% endif %}
177179

178180
model Id1 {
@@ -211,6 +213,7 @@ model Id5 {
211213

212214
@@unique([name, role])
213215
}
216+
214217
{% endif %}
215218

216219
{% if config.supports_feature('enum') %}
@@ -219,4 +222,5 @@ enum Role {
219222
ADMIN
220223
EDITOR
221224
}
225+
222226
{% endif %}

databases/tests/test_metrics.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
3+
from prisma import Prisma
4+
from prisma._compat import model_json
5+
6+
7+
@pytest.mark.asyncio
8+
async def test_prometheus(client: Prisma) -> None:
9+
"""Metrics can be returned in Prometheus format"""
10+
await client.user.create(data={'name': 'Robert'})
11+
12+
metrics = await client.get_metrics(format='prometheus')
13+
14+
assert 'prisma_client_queries_total' in metrics
15+
assert 'prisma_datasource_queries_total' in metrics
16+
assert 'prisma_client_queries_active' in metrics
17+
assert 'prisma_client_queries_duration_histogram_ms_bucket' in metrics
18+
19+
20+
@pytest.mark.asyncio
21+
async def test_json_string(client: Prisma) -> None:
22+
"""Metrics can be serlialized to JSON"""
23+
await client.user.create(data={'name': 'Robert'})
24+
25+
metrics = await client.get_metrics()
26+
assert isinstance(model_json(metrics), str)
27+
28+
29+
@pytest.mark.asyncio
30+
async def test_json(client: Prisma) -> None:
31+
"""Metrics returned in the JSON format"""
32+
await client.user.create(data={'name': 'Robert'})
33+
34+
metrics = await client.get_metrics(format='json')
35+
36+
assert len(metrics.counters) > 0
37+
assert metrics.counters[0].value > 0
38+
39+
assert len(metrics.gauges) > 0
40+
gauge = next(
41+
filter(
42+
lambda g: g.key == 'prisma_pool_connections_open', metrics.gauges
43+
)
44+
)
45+
assert gauge.value > 0
46+
47+
assert len(metrics.histograms) > 0
48+
assert metrics.histograms[0].value.sum > 0
49+
assert metrics.histograms[0].value.count > 0
50+
51+
assert len(metrics.histograms[0].value.buckets) > 0
52+
53+
for bucket in metrics.histograms[0].value.buckets:
54+
assert bucket.max_value >= 0
55+
assert bucket.total_count >= 0
56+
57+
58+
@pytest.mark.asyncio
59+
async def test_global_labels(client: Prisma) -> None:
60+
metrics = await client.get_metrics(global_labels={'foo': 'bar'})
61+
assert metrics.counters[0].labels == {'foo': 'bar'}

docs/reference/metrics.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Metrics
2+
3+
Prisma Metrics gives you detailed insight into how Prisma is interactting with your Database. For more detailed information see the [Prisma Documentation](https://www.prisma.io/docs/concepts/components/prisma-client/metrics).
4+
5+
To access metrics in the Python client
6+
7+
Metrics can be accessed in the Python client through the `Prisma.get_metrics()` method. Two different formats are available, `json` and `prometheus`.
8+
9+
## JSON Format
10+
11+
The default format is `json` which returns a `prisma.Metrics` instance:
12+
13+
```py
14+
from prisma import Prisma
15+
16+
client = Prisma()
17+
18+
metrics = client.get_metrics()
19+
print(metrics.counters[0])
20+
```
21+
22+
See the [Prisma Documentation](https://www.prisma.io/docs/concepts/components/prisma-client/metrics#retrieve-metrics-in-json-format) for more details on the structure of the metrics object.
23+
24+
## Prometheus Format
25+
26+
The `prometheus` format returns a `str` which is a valid [Prometheus data](https://prometheus.io/).
27+
28+
29+
```py
30+
from prisma import Prisma
31+
32+
client = Prisma()
33+
34+
metrics = client.get_metrics(format='prometheus')
35+
print(metrics)
36+
```
37+
38+
See the [Prisma Documentation](https://www.prisma.io/docs/concepts/components/prisma-client/metrics#retrieve-metrics-in-prometheus-format) for more details on the structure of the data.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ nav:
7474
- Custom Generators: reference/custom-generators.md
7575
- Logging: reference/logging.md
7676
- Binaries: reference/binaries.md
77+
- Metrics: reference/metrics.md
7778
- Contributing:
7879
- How to Contribute: contributing/contributing.md
7980
- Architecture: contributing/architecture.md

src/prisma/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
from . import errors as errors
1414
from .validator import *
1515
from ._types import PrismaMethod as PrismaMethod
16+
from ._metrics import (
17+
Metric as Metric,
18+
Metrics as Metrics,
19+
MetricHistogram as MetricHistogram,
20+
)
1621

1722

1823
try:

src/prisma/_metrics.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# copied from https://github.com/prisma/prisma/blob/23d5ef0672372035a84552b6b457197ca19f486d/packages/client/src/runtime/core/engines/common/types/Metrics.ts
2+
from __future__ import annotations
3+
4+
from typing import Generic, List, TypeVar, Dict, NamedTuple
5+
6+
from pydantic import BaseModel
7+
8+
from ._compat import GenericModel, model_rebuild
9+
10+
11+
__all__ = (
12+
'Metrics',
13+
'Metric',
14+
'MetricHistogram',
15+
)
16+
17+
18+
_T = TypeVar('_T')
19+
20+
21+
# TODO: check if int / float is right
22+
23+
24+
class Metrics(BaseModel):
25+
counters: List[Metric[int]]
26+
gauges: List[Metric[float]]
27+
histograms: List[Metric[MetricHistogram]]
28+
29+
30+
class Metric(GenericModel, Generic[_T]):
31+
key: str
32+
value: _T
33+
labels: Dict[str, str]
34+
description: str
35+
36+
37+
class MetricHistogram(BaseModel):
38+
sum: float
39+
count: int
40+
buckets: List[HistogramBucket]
41+
42+
43+
class HistogramBucket(NamedTuple):
44+
max_value: float
45+
total_count: int
46+
47+
48+
model_rebuild(Metric)
49+
model_rebuild(Metrics)
50+
model_rebuild(MetricHistogram)

src/prisma/generator/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,9 @@ def warn_binary_targets(
480480

481481
return targets
482482

483+
def has_preview_feature(self, feature: str) -> bool:
484+
return feature in self.preview_features
485+
483486

484487
class ValueFromEnvVar(BaseModel):
485488
value: str

src/prisma/generator/templates/client.py.jinja

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ from types import TracebackType
1111
from pydantic import BaseModel
1212

1313
from . import types, models, errors, actions
14-
from .types import DatasourceOverride, HttpConfig
14+
from .types import DatasourceOverride, HttpConfig, MetricsFormat
1515
from ._types import BaseModelT, PrismaMethod
1616
from .bases import _PrismaModel
1717
from .engine import AbstractEngine, QueryEngine, TransactionId
@@ -20,6 +20,7 @@ from .generator.models import EngineType, OptionalValueFromEnvVar, BinaryPaths
2020
from ._compat import removeprefix, model_parse
2121
from ._constants import DEFAULT_CONNECT_TIMEOUT, DEFAULT_TX_MAX_WAIT, DEFAULT_TX_TIMEOUT
2222
from ._raw_query import deserialize_raw_results
23+
from ._metrics import Metrics
2324

2425
__all__ = (
2526
'ENGINE_TYPE',
@@ -423,6 +424,44 @@ class Prisma:
423424
"""Returns True if the client is wrapped within a transaction"""
424425
return self._tx_id is not None
425426

427+
@overload
428+
{{ maybe_async_def }}get_metrics(
429+
self,
430+
format: Literal['json'] = 'json',
431+
*,
432+
global_labels: dict[str, str] | None = None,
433+
) -> Metrics:
434+
...
435+
436+
@overload
437+
{{ maybe_async_def }}get_metrics(
438+
self,
439+
format: Literal['prometheus'],
440+
*,
441+
global_labels: dict[str, str] | None = None,
442+
) -> str:
443+
...
444+
445+
{{ maybe_async_def }}get_metrics(
446+
self,
447+
format: MetricsFormat = 'json',
448+
*,
449+
global_labels: dict[str, str] | None = None,
450+
) -> str | Metrics:
451+
"""Metrics give you a detailed insight into how the Prisma Client interacts with your database.
452+
453+
You can retrieve metrics in either JSON or Prometheus formats.
454+
455+
For more details see https://www.prisma.io/docs/concepts/components/prisma-client/metrics.
456+
"""
457+
response = {{ maybe_await }}self._engine.metrics(format=format, global_labels=global_labels)
458+
if format == 'prometheus':
459+
# For the prometheus format we return the response as-is
460+
assert isinstance(response, str)
461+
return response
462+
463+
return model_parse(Metrics, response)
464+
426465
# TODO: don't return Any
427466
{{ maybe_async_def }}_execute(
428467
self,

0 commit comments

Comments
 (0)