fastapi-ext-pkg 0.4.4__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_ext_pkg-0.7.0/PKG-INFO +74 -0
- fastapi_ext_pkg-0.7.0/README.md +49 -0
- fastapi_ext_pkg-0.7.0/fastapi_ext_pkg.egg-info/PKG-INFO +74 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_ext_pkg.egg-info/SOURCES.txt +11 -1
- fastapi_ext_pkg-0.7.0/fastapi_ext_pkg.egg-info/requires.txt +5 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/__init__.py +19 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/config.py +66 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/constants.py +46 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/decorators.py +127 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/labels.py +20 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/masking.py +87 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/middleware.py +105 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/apm/processors.py +80 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_extras/databases/redis.py +13 -2
- fastapi_ext_pkg-0.7.0/fastapi_extras/middlewares/__init__.py +3 -0
- fastapi_ext_pkg-0.7.0/fastapi_extras/middlewares/configure.py +98 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_extras/security/auth.py +38 -16
- fastapi_ext_pkg-0.7.0/fastapi_extras/security/auth_async.py +180 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/pyproject.toml +4 -1
- fastapi_ext_pkg-0.4.4/PKG-INFO +0 -42
- fastapi_ext_pkg-0.4.4/README.md +0 -19
- fastapi_ext_pkg-0.4.4/fastapi_ext_pkg.egg-info/PKG-INFO +0 -42
- fastapi_ext_pkg-0.4.4/fastapi_ext_pkg.egg-info/requires.txt +0 -2
- fastapi_ext_pkg-0.4.4/fastapi_extras/security/__init__.py +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_ext_pkg.egg-info/dependency_links.txt +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_ext_pkg.egg-info/top_level.txt +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_extras/__init__.py +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_extras/databases/__init__.py +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_extras/middlewares/remove_cloud_trace_headers.py +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_extras/middlewares/remove_path.py +0 -0
- {fastapi_ext_pkg-0.4.4/fastapi_extras/middlewares → fastapi_ext_pkg-0.7.0/fastapi_extras/orm}/__init__.py +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/fastapi_extras/orm/sqlalchemy.py +0 -0
- {fastapi_ext_pkg-0.4.4/fastapi_extras/orm → fastapi_ext_pkg-0.7.0/fastapi_extras/security}/__init__.py +0 -0
- {fastapi_ext_pkg-0.4.4 → fastapi_ext_pkg-0.7.0}/setup.cfg +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-ext-pkg
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: A well-tailored utility set for FastAPI.
|
|
5
|
+
Author-email: Dotz Developers <devs-dotz@dotz.com>
|
|
6
|
+
Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
|
|
7
|
+
Keywords: fastapi,utilities
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Internet
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: fastapi
|
|
22
|
+
Requires-Dist: httpx
|
|
23
|
+
Provides-Extra: apm
|
|
24
|
+
Requires-Dist: elastic-apm<7,>=6.20; extra == "apm"
|
|
25
|
+
|
|
26
|
+
# FastAPI Extension Package
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+

|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
A well-tailored utility set for FastAPI.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
* Python 3.8+
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
To install FastAPI Extension Package, use pip:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install fastapi-ext-pkg
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Shared middleware configuration
|
|
47
|
+
|
|
48
|
+
`configure_middlewares` registers middleware on an existing FastAPI application
|
|
49
|
+
before startup. Configure service values centrally while preserving routers and
|
|
50
|
+
lifespan:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fastapi_extras.middlewares import configure_middlewares
|
|
54
|
+
|
|
55
|
+
configure_middlewares(
|
|
56
|
+
app,
|
|
57
|
+
root_path=settings.ROOT_PATH,
|
|
58
|
+
apm_config=settings.ELASTIC_APM,
|
|
59
|
+
cors={"allow_origins": settings.ALLOW_ORIGINS},
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The default registration order is path removal, trace-header removal, APM,
|
|
64
|
+
`extra_middlewares`, then CORS. Incoming requests traverse it in reverse. Use
|
|
65
|
+
`order` to preserve a service-specific order; include every configured name
|
|
66
|
+
exactly once. `MiddlewareSpec` defines custom middleware and named `overrides`
|
|
67
|
+
for replacement or disabling (`enabled=False`).
|
|
68
|
+
|
|
69
|
+
Use `root_path=None`, `remove_cloud_trace_headers=False`, `apm_enabled=False`,
|
|
70
|
+
or `cors=None` to omit middleware. Pass either `apm_config` or `apm_client`.
|
|
71
|
+
APM uses the upstream `elasticapm.contrib.starlette.ElasticAPM` by default and
|
|
72
|
+
requires the optional `[apm]` dependency. This branch contains no custom APM
|
|
73
|
+
implementation. To register another implementation supplied by the caller, pass
|
|
74
|
+
`apm_middleware_class`; `apm_options` supplies its constructor options.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# FastAPI Extension Package
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A well-tailored utility set for FastAPI.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
* Python 3.8+
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
To install FastAPI Extension Package, use pip:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
pip install fastapi-ext-pkg
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Shared middleware configuration
|
|
22
|
+
|
|
23
|
+
`configure_middlewares` registers middleware on an existing FastAPI application
|
|
24
|
+
before startup. Configure service values centrally while preserving routers and
|
|
25
|
+
lifespan:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from fastapi_extras.middlewares import configure_middlewares
|
|
29
|
+
|
|
30
|
+
configure_middlewares(
|
|
31
|
+
app,
|
|
32
|
+
root_path=settings.ROOT_PATH,
|
|
33
|
+
apm_config=settings.ELASTIC_APM,
|
|
34
|
+
cors={"allow_origins": settings.ALLOW_ORIGINS},
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The default registration order is path removal, trace-header removal, APM,
|
|
39
|
+
`extra_middlewares`, then CORS. Incoming requests traverse it in reverse. Use
|
|
40
|
+
`order` to preserve a service-specific order; include every configured name
|
|
41
|
+
exactly once. `MiddlewareSpec` defines custom middleware and named `overrides`
|
|
42
|
+
for replacement or disabling (`enabled=False`).
|
|
43
|
+
|
|
44
|
+
Use `root_path=None`, `remove_cloud_trace_headers=False`, `apm_enabled=False`,
|
|
45
|
+
or `cors=None` to omit middleware. Pass either `apm_config` or `apm_client`.
|
|
46
|
+
APM uses the upstream `elasticapm.contrib.starlette.ElasticAPM` by default and
|
|
47
|
+
requires the optional `[apm]` dependency. This branch contains no custom APM
|
|
48
|
+
implementation. To register another implementation supplied by the caller, pass
|
|
49
|
+
`apm_middleware_class`; `apm_options` supplies its constructor options.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-ext-pkg
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: A well-tailored utility set for FastAPI.
|
|
5
|
+
Author-email: Dotz Developers <devs-dotz@dotz.com>
|
|
6
|
+
Project-URL: Repository, https://github.com/DotzInc/fastapi-ext-pkg
|
|
7
|
+
Keywords: fastapi,utilities
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Internet
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: fastapi
|
|
22
|
+
Requires-Dist: httpx
|
|
23
|
+
Provides-Extra: apm
|
|
24
|
+
Requires-Dist: elastic-apm<7,>=6.20; extra == "apm"
|
|
25
|
+
|
|
26
|
+
# FastAPI Extension Package
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+

|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
A well-tailored utility set for FastAPI.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
* Python 3.8+
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
To install FastAPI Extension Package, use pip:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install fastapi-ext-pkg
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Shared middleware configuration
|
|
47
|
+
|
|
48
|
+
`configure_middlewares` registers middleware on an existing FastAPI application
|
|
49
|
+
before startup. Configure service values centrally while preserving routers and
|
|
50
|
+
lifespan:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fastapi_extras.middlewares import configure_middlewares
|
|
54
|
+
|
|
55
|
+
configure_middlewares(
|
|
56
|
+
app,
|
|
57
|
+
root_path=settings.ROOT_PATH,
|
|
58
|
+
apm_config=settings.ELASTIC_APM,
|
|
59
|
+
cors={"allow_origins": settings.ALLOW_ORIGINS},
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The default registration order is path removal, trace-header removal, APM,
|
|
64
|
+
`extra_middlewares`, then CORS. Incoming requests traverse it in reverse. Use
|
|
65
|
+
`order` to preserve a service-specific order; include every configured name
|
|
66
|
+
exactly once. `MiddlewareSpec` defines custom middleware and named `overrides`
|
|
67
|
+
for replacement or disabling (`enabled=False`).
|
|
68
|
+
|
|
69
|
+
Use `root_path=None`, `remove_cloud_trace_headers=False`, `apm_enabled=False`,
|
|
70
|
+
or `cors=None` to omit middleware. Pass either `apm_config` or `apm_client`.
|
|
71
|
+
APM uses the upstream `elasticapm.contrib.starlette.ElasticAPM` by default and
|
|
72
|
+
requires the optional `[apm]` dependency. This branch contains no custom APM
|
|
73
|
+
implementation. To register another implementation supplied by the caller, pass
|
|
74
|
+
`apm_middleware_class`; `apm_options` supplies its constructor options.
|
|
@@ -6,12 +6,22 @@ fastapi_ext_pkg.egg-info/dependency_links.txt
|
|
|
6
6
|
fastapi_ext_pkg.egg-info/requires.txt
|
|
7
7
|
fastapi_ext_pkg.egg-info/top_level.txt
|
|
8
8
|
fastapi_extras/__init__.py
|
|
9
|
+
fastapi_extras/apm/__init__.py
|
|
10
|
+
fastapi_extras/apm/config.py
|
|
11
|
+
fastapi_extras/apm/constants.py
|
|
12
|
+
fastapi_extras/apm/decorators.py
|
|
13
|
+
fastapi_extras/apm/labels.py
|
|
14
|
+
fastapi_extras/apm/masking.py
|
|
15
|
+
fastapi_extras/apm/middleware.py
|
|
16
|
+
fastapi_extras/apm/processors.py
|
|
9
17
|
fastapi_extras/databases/__init__.py
|
|
10
18
|
fastapi_extras/databases/redis.py
|
|
11
19
|
fastapi_extras/middlewares/__init__.py
|
|
20
|
+
fastapi_extras/middlewares/configure.py
|
|
12
21
|
fastapi_extras/middlewares/remove_cloud_trace_headers.py
|
|
13
22
|
fastapi_extras/middlewares/remove_path.py
|
|
14
23
|
fastapi_extras/orm/__init__.py
|
|
15
24
|
fastapi_extras/orm/sqlalchemy.py
|
|
16
25
|
fastapi_extras/security/__init__.py
|
|
17
|
-
fastapi_extras/security/auth.py
|
|
26
|
+
fastapi_extras/security/auth.py
|
|
27
|
+
fastapi_extras/security/auth_async.py
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from fastapi_extras.apm.config import build_apm_config
|
|
2
|
+
from fastapi_extras.apm.constants import DEFAULT_SANITIZE_FIELD_NAMES
|
|
3
|
+
from fastapi_extras.apm.decorators import capture_http_request
|
|
4
|
+
from fastapi_extras.apm.labels import label_transaction
|
|
5
|
+
from fastapi_extras.apm.masking import mask_sensitive_data, normalize_span_value
|
|
6
|
+
from fastapi_extras.apm.middleware import ElasticAPMMiddleware
|
|
7
|
+
from fastapi_extras.apm.processors import ALL_PROCESSORS, PROCESSORS
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ALL_PROCESSORS",
|
|
11
|
+
"DEFAULT_SANITIZE_FIELD_NAMES",
|
|
12
|
+
"PROCESSORS",
|
|
13
|
+
"ElasticAPMMiddleware",
|
|
14
|
+
"build_apm_config",
|
|
15
|
+
"capture_http_request",
|
|
16
|
+
"label_transaction",
|
|
17
|
+
"mask_sensitive_data",
|
|
18
|
+
"normalize_span_value",
|
|
19
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
import fastapi
|
|
4
|
+
|
|
5
|
+
from fastapi_extras.apm.constants import (
|
|
6
|
+
DEFAULT_CAPTURE_BODY,
|
|
7
|
+
DEFAULT_CLOUD_PROVIDER,
|
|
8
|
+
DEFAULT_LOG_LEVEL,
|
|
9
|
+
DEFAULT_SANITIZE_FIELD_NAMES,
|
|
10
|
+
)
|
|
11
|
+
from fastapi_extras.apm.processors import ALL_PROCESSORS
|
|
12
|
+
|
|
13
|
+
FieldNamesInput = Union[str, Iterable[str]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_field_names(value: FieldNamesInput) -> List[str]:
|
|
17
|
+
items = value.split(",") if isinstance(value, str) else value
|
|
18
|
+
return [item.strip() for item in items if item.strip()]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def unique(items: Iterable[str]) -> List[str]:
|
|
22
|
+
return list(dict.fromkeys(items))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def has_credentials(server_url: Optional[str], secret_token: Optional[str]) -> bool:
|
|
26
|
+
return bool((server_url or "").strip() and (secret_token or "").strip())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_apm_config(
|
|
30
|
+
service_name: str,
|
|
31
|
+
*,
|
|
32
|
+
server_url: Optional[str] = "",
|
|
33
|
+
secret_token: Optional[str] = "",
|
|
34
|
+
environment: str = "",
|
|
35
|
+
enabled: Optional[bool] = None,
|
|
36
|
+
log_level: str = DEFAULT_LOG_LEVEL,
|
|
37
|
+
capture_body: str = DEFAULT_CAPTURE_BODY,
|
|
38
|
+
sanitize_field_names: Optional[FieldNamesInput] = None,
|
|
39
|
+
extra_sanitize_field_names: FieldNamesInput = (),
|
|
40
|
+
extra_processors: Iterable[str] = (),
|
|
41
|
+
**overrides: Any,
|
|
42
|
+
) -> Dict[str, Any]:
|
|
43
|
+
base_field_names = (
|
|
44
|
+
DEFAULT_SANITIZE_FIELD_NAMES
|
|
45
|
+
if sanitize_field_names is None
|
|
46
|
+
else parse_field_names(sanitize_field_names)
|
|
47
|
+
)
|
|
48
|
+
config: Dict[str, Any] = {
|
|
49
|
+
"ENABLED": has_credentials(server_url, secret_token) if enabled is None else enabled,
|
|
50
|
+
"SERVICE_NAME": service_name,
|
|
51
|
+
"SECRET_TOKEN": secret_token or "",
|
|
52
|
+
"SERVER_URL": server_url or "",
|
|
53
|
+
"ENVIRONMENT": environment,
|
|
54
|
+
"LOG_LEVEL": log_level,
|
|
55
|
+
"CLOUD_PROVIDER": DEFAULT_CLOUD_PROVIDER,
|
|
56
|
+
"FRAMEWORK_NAME": "fastapi",
|
|
57
|
+
"FRAMEWORK_VERSION": fastapi.__version__,
|
|
58
|
+
"CAPTURE_HEADERS": True,
|
|
59
|
+
"CAPTURE_BODY": capture_body,
|
|
60
|
+
"SANITIZE_FIELD_NAMES": unique(
|
|
61
|
+
[*base_field_names, *parse_field_names(extra_sanitize_field_names)]
|
|
62
|
+
),
|
|
63
|
+
"PROCESSORS": (*ALL_PROCESSORS, *extra_processors),
|
|
64
|
+
}
|
|
65
|
+
config.update({key.upper(): value for key, value in overrides.items()})
|
|
66
|
+
return config
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
EXTERNAL = "external"
|
|
2
|
+
UNKNOWN = "UNKNOWN"
|
|
3
|
+
UNKNOWN_STATUS_CODE = 999
|
|
4
|
+
MAX_FIELD_LENGTH = 500
|
|
5
|
+
MAX_RESPONSE_BODY_BYTES = 64 * 1024
|
|
6
|
+
TRUNCATED_SUFFIX = "… [TRUNCATED]"
|
|
7
|
+
UNREADABLE_RESPONSE = "<could not decode response>"
|
|
8
|
+
RESPONSE_BODY_TOO_LARGE = "<response body too large>"
|
|
9
|
+
RESPONSE_BODY_CONTEXT_KEY = "response_body"
|
|
10
|
+
JSON_MEDIA_TYPE = "application/json"
|
|
11
|
+
JSON_MEDIA_TYPE_SUFFIX = "+json"
|
|
12
|
+
IDENTITY_ENCODING = "identity"
|
|
13
|
+
DEFAULT_SANITIZE_FIELD_NAMES = (
|
|
14
|
+
"password",
|
|
15
|
+
"passwd",
|
|
16
|
+
"pwd",
|
|
17
|
+
"secret",
|
|
18
|
+
"*key",
|
|
19
|
+
"*token*",
|
|
20
|
+
"*session*",
|
|
21
|
+
"*credit*",
|
|
22
|
+
"*card*",
|
|
23
|
+
"*auth*",
|
|
24
|
+
"*principal*",
|
|
25
|
+
"set-cookie",
|
|
26
|
+
"pin",
|
|
27
|
+
"otp",
|
|
28
|
+
"*document*",
|
|
29
|
+
"*cpf*",
|
|
30
|
+
"*cnpj*",
|
|
31
|
+
"*email*",
|
|
32
|
+
"*phone*",
|
|
33
|
+
"*mobile*",
|
|
34
|
+
"*birth*",
|
|
35
|
+
"mother_name",
|
|
36
|
+
"rg",
|
|
37
|
+
"*account_number*",
|
|
38
|
+
"agency",
|
|
39
|
+
"pix_key",
|
|
40
|
+
"*cvv*",
|
|
41
|
+
"*salary*",
|
|
42
|
+
"*income*",
|
|
43
|
+
)
|
|
44
|
+
DEFAULT_LOG_LEVEL = "INFO"
|
|
45
|
+
DEFAULT_CAPTURE_BODY = "off"
|
|
46
|
+
DEFAULT_CLOUD_PROVIDER = "gcp"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Any, Awaitable, Callable, Dict, Mapping, Optional, Tuple, TypeVar
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
import elasticapm
|
|
7
|
+
from elasticapm.conf.constants import MASK
|
|
8
|
+
|
|
9
|
+
from fastapi_extras.apm.constants import EXTERNAL, UNKNOWN, UNKNOWN_STATUS_CODE, UNREADABLE_RESPONSE
|
|
10
|
+
from fastapi_extras.apm.labels import to_label_value
|
|
11
|
+
from fastapi_extras.apm.masking import (
|
|
12
|
+
FieldNames,
|
|
13
|
+
get_sanitize_field_names,
|
|
14
|
+
is_sensitive_key,
|
|
15
|
+
mask_sensitive_data,
|
|
16
|
+
normalize_span_value,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ReturnType = TypeVar("ReturnType")
|
|
20
|
+
|
|
21
|
+
ERROR_BODY_ATTRIBUTES = ("errors", "content", "detail")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_span_name(method: str, url: str) -> str:
|
|
25
|
+
parsed = urlparse(url)
|
|
26
|
+
path = parsed.path
|
|
27
|
+
if parsed.query:
|
|
28
|
+
path = f"{path}?{parsed.query}"
|
|
29
|
+
return f"{method.upper()} {path}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_outgoing_request(value: Any) -> bool:
|
|
33
|
+
return isinstance(getattr(value, "method", None), str) and isinstance(
|
|
34
|
+
getattr(value, "url", None), str
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_outgoing_request(args: Tuple[Any, ...], kwargs: Mapping[str, Any]) -> Any:
|
|
39
|
+
candidate = kwargs.get("request")
|
|
40
|
+
if is_outgoing_request(candidate):
|
|
41
|
+
return candidate
|
|
42
|
+
return next((arg for arg in args if is_outgoing_request(arg)), None)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def read_response(response: Any) -> Tuple[Optional[int], Any]:
|
|
46
|
+
status_code = getattr(response, "status", None)
|
|
47
|
+
if not isinstance(status_code, int):
|
|
48
|
+
status_code = getattr(response, "status_code", None)
|
|
49
|
+
if not isinstance(status_code, int):
|
|
50
|
+
return None, None
|
|
51
|
+
try:
|
|
52
|
+
text = response.text
|
|
53
|
+
body = text() if callable(text) else text
|
|
54
|
+
if inspect.isawaitable(body):
|
|
55
|
+
body = await body
|
|
56
|
+
except Exception:
|
|
57
|
+
body = UNREADABLE_RESPONSE
|
|
58
|
+
return status_code, body
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def read_error(error: Exception) -> Tuple[int, Any]:
|
|
62
|
+
status_code = getattr(error, "status_code", None)
|
|
63
|
+
if not isinstance(status_code, int):
|
|
64
|
+
return UNKNOWN_STATUS_CODE, repr(error)
|
|
65
|
+
body = {}
|
|
66
|
+
for attribute in ERROR_BODY_ATTRIBUTES:
|
|
67
|
+
value = getattr(error, attribute, None)
|
|
68
|
+
if value:
|
|
69
|
+
body[attribute] = value
|
|
70
|
+
return status_code, body
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_span_labels(
|
|
74
|
+
status_code: Optional[int],
|
|
75
|
+
response_body: Any,
|
|
76
|
+
payload: Any,
|
|
77
|
+
extra_labels: Optional[Mapping[str, Any]] = None,
|
|
78
|
+
field_names: Optional[FieldNames] = None,
|
|
79
|
+
) -> Dict[str, Any]:
|
|
80
|
+
names = get_sanitize_field_names() if field_names is None else field_names
|
|
81
|
+
labels: Dict[str, Any] = {
|
|
82
|
+
"status_code": status_code,
|
|
83
|
+
"response_body": normalize_span_value(response_body),
|
|
84
|
+
}
|
|
85
|
+
if payload:
|
|
86
|
+
labels["payload"] = normalize_span_value(payload)
|
|
87
|
+
if extra_labels:
|
|
88
|
+
labels.update(extra_labels)
|
|
89
|
+
return {
|
|
90
|
+
key: MASK
|
|
91
|
+
if is_sensitive_key(key, names)
|
|
92
|
+
else to_label_value(mask_sensitive_data(value, names))
|
|
93
|
+
for key, value in labels.items()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def capture_http_request(
|
|
98
|
+
func: Callable[..., Awaitable[ReturnType]],
|
|
99
|
+
) -> Callable[..., Awaitable[ReturnType]]:
|
|
100
|
+
@functools.wraps(func)
|
|
101
|
+
async def wrapper(*args: Any, **kwargs: Any) -> ReturnType:
|
|
102
|
+
request = find_outgoing_request(args, kwargs)
|
|
103
|
+
method = getattr(request, "method", UNKNOWN)
|
|
104
|
+
url = getattr(request, "url", UNKNOWN)
|
|
105
|
+
payload = getattr(request, "payload", None)
|
|
106
|
+
status_code: Optional[int] = None
|
|
107
|
+
response_body: Any = None
|
|
108
|
+
|
|
109
|
+
async with elasticapm.async_capture_span(
|
|
110
|
+
get_span_name(method, url), span_type=EXTERNAL
|
|
111
|
+
) as span:
|
|
112
|
+
try:
|
|
113
|
+
response = await func(*args, **kwargs)
|
|
114
|
+
status_code, response_body = await read_response(response)
|
|
115
|
+
return response
|
|
116
|
+
except Exception as error:
|
|
117
|
+
status_code, response_body = read_error(error)
|
|
118
|
+
raise
|
|
119
|
+
finally:
|
|
120
|
+
if span:
|
|
121
|
+
span.label(
|
|
122
|
+
**build_span_labels(
|
|
123
|
+
status_code, response_body, payload, kwargs.get("labels")
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return wrapper
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Iterable, Mapping
|
|
3
|
+
|
|
4
|
+
import elasticapm
|
|
5
|
+
|
|
6
|
+
from fastapi_extras.apm.masking import get_sanitize_field_names, sanitize, truncate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def to_label_value(value: Any) -> Any:
|
|
10
|
+
if isinstance(value, (dict, list)):
|
|
11
|
+
return json.dumps(value, ensure_ascii=False, default=str)
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def label_transaction(data: Mapping[str, Any], keys: Iterable[str]) -> None:
|
|
16
|
+
labels = {key: data[key] for key in keys if data.get(key) is not None}
|
|
17
|
+
if not labels:
|
|
18
|
+
return
|
|
19
|
+
safe_labels = truncate(sanitize(labels, get_sanitize_field_names()))
|
|
20
|
+
elasticapm.label(**{key: to_label_value(value) for key, value in safe_labels.items()})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Optional, Pattern, Sequence
|
|
5
|
+
|
|
6
|
+
import elasticapm
|
|
7
|
+
from elasticapm.conf.constants import BASE_SANITIZE_FIELD_NAMES, MASK
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from fastapi_extras.apm.constants import MAX_FIELD_LENGTH, TRUNCATED_SUFFIX
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
FieldNames = Sequence[Pattern]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_sanitize_field_names() -> FieldNames:
|
|
18
|
+
client = elasticapm.get_client()
|
|
19
|
+
if client is None:
|
|
20
|
+
return BASE_SANITIZE_FIELD_NAMES
|
|
21
|
+
return client.config.sanitize_field_names
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_sensitive_key(key: Any, field_names: FieldNames) -> bool:
|
|
25
|
+
if not isinstance(key, str):
|
|
26
|
+
return False
|
|
27
|
+
normalized = key.strip().lower()
|
|
28
|
+
return any(pattern.match(normalized) for pattern in field_names)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sanitize(data: Any, field_names: FieldNames) -> Any:
|
|
32
|
+
if isinstance(data, dict):
|
|
33
|
+
return {
|
|
34
|
+
key: MASK if is_sensitive_key(key, field_names) else sanitize(value, field_names)
|
|
35
|
+
for key, value in data.items()
|
|
36
|
+
}
|
|
37
|
+
if isinstance(data, (list, tuple)):
|
|
38
|
+
return [sanitize(item, field_names) for item in data]
|
|
39
|
+
return data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def truncate(data: Any, max_length: int = MAX_FIELD_LENGTH) -> Any:
|
|
43
|
+
if isinstance(data, dict):
|
|
44
|
+
return {key: truncate(value, max_length) for key, value in data.items()}
|
|
45
|
+
if isinstance(data, (list, tuple)):
|
|
46
|
+
return [truncate(item, max_length) for item in data]
|
|
47
|
+
if data is None or isinstance(data, (bool, int, float)):
|
|
48
|
+
return data
|
|
49
|
+
text = data if isinstance(data, str) else str(data)
|
|
50
|
+
if len(text) > max_length:
|
|
51
|
+
return text[:max_length] + TRUNCATED_SUFFIX
|
|
52
|
+
return text
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normalize_span_value(value: Any) -> Any:
|
|
56
|
+
if isinstance(value, BaseModel):
|
|
57
|
+
dump = getattr(value, "model_dump", None) or value.dict
|
|
58
|
+
return dump()
|
|
59
|
+
if isinstance(value, (bytes, bytearray)):
|
|
60
|
+
try:
|
|
61
|
+
return bytes(value).decode("utf-8")
|
|
62
|
+
except UnicodeDecodeError:
|
|
63
|
+
return base64.b64encode(bytes(value)).decode("ascii")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_json(value: str) -> Any:
|
|
68
|
+
try:
|
|
69
|
+
return json.loads(value)
|
|
70
|
+
except ValueError:
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def mask_sensitive_data(
|
|
75
|
+
data: Any,
|
|
76
|
+
field_names: Optional[FieldNames] = None,
|
|
77
|
+
max_length: int = MAX_FIELD_LENGTH,
|
|
78
|
+
) -> Any:
|
|
79
|
+
names = get_sanitize_field_names() if field_names is None else field_names
|
|
80
|
+
try:
|
|
81
|
+
value = normalize_span_value(data)
|
|
82
|
+
if isinstance(value, str):
|
|
83
|
+
value = parse_json(value)
|
|
84
|
+
return truncate(sanitize(value, names), max_length)
|
|
85
|
+
except Exception:
|
|
86
|
+
logger.exception("Could not mask APM data")
|
|
87
|
+
return MASK
|