hexkit 5.4.1__tar.gz → 7.0.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.
Files changed (66) hide show
  1. {hexkit-5.4.1/src/hexkit.egg-info → hexkit-7.0.0}/PKG-INFO +12 -11
  2. {hexkit-5.4.1 → hexkit-7.0.0}/README.md +1 -1
  3. {hexkit-5.4.1 → hexkit-7.0.0}/pyproject.toml +15 -15
  4. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/correlation.py +33 -16
  5. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/log.py +1 -3
  6. hexkit-7.0.0/src/hexkit/opentelemetry.py +76 -0
  7. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/protocols/eventpub.py +17 -2
  8. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/protocols/eventsub.py +12 -1
  9. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/config.py +2 -2
  10. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/provider/eventpub.py +29 -15
  11. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/provider/eventsub.py +105 -53
  12. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/testutils.py +28 -10
  13. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongodb/migrations/_manager.py +8 -7
  14. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongodb/migrations/_utils.py +2 -2
  15. hexkit-7.0.0/src/hexkit/providers/mongodb/migrations/helpers.py +97 -0
  16. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongodb/provider.py +110 -67
  17. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongodb/testutils.py +37 -18
  18. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongokafka/provider/daopub.py +40 -52
  19. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +33 -15
  20. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/testing/eventpub.py +3 -1
  21. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/utils.py +25 -0
  22. {hexkit-5.4.1 → hexkit-7.0.0/src/hexkit.egg-info}/PKG-INFO +12 -11
  23. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit.egg-info/SOURCES.txt +1 -1
  24. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit.egg-info/requires.txt +10 -9
  25. hexkit-5.4.1/src/hexkit/opentelemetry.py +0 -177
  26. hexkit-5.4.1/src/hexkit.egg-info/entry_points.txt +0 -2
  27. {hexkit-5.4.1 → hexkit-7.0.0}/LICENSE +0 -0
  28. {hexkit-5.4.1 → hexkit-7.0.0}/MANIFEST.in +0 -0
  29. {hexkit-5.4.1 → hexkit-7.0.0}/setup.cfg +0 -0
  30. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/__init__.py +0 -0
  31. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/__main__.py +0 -0
  32. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/base.py +0 -0
  33. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/config.py +0 -0
  34. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/custom_types.py +0 -0
  35. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/protocols/__init__.py +0 -0
  36. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/protocols/dao.py +0 -0
  37. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/protocols/daopub.py +0 -0
  38. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/protocols/daosub.py +0 -0
  39. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/protocols/objstorage.py +0 -0
  40. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/__init__.py +0 -0
  41. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  42. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  43. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  44. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  45. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  46. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  47. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
  48. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  49. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
  50. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongokafka/provider/config.py +0 -0
  51. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  52. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/__init__.py +0 -0
  53. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/provider.py +0 -0
  54. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  55. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  56. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  57. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  58. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  59. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
  60. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
  61. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  62. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
  63. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/providers/testing/__init__.py +0 -0
  64. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit/py.typed +0 -0
  65. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  66. {hexkit-5.4.1 → hexkit-7.0.0}/src/hexkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexkit
3
- Version: 5.4.1
3
+ Version: 7.0.0
4
4
  Summary: A Toolkit for Building Microservices using the Hexagonal Architecture
5
5
  Author-email: "German Human Genome Phenome Archive (GHGA)" <contact@ghga.de>
6
6
  License: Apache 2.0
@@ -30,27 +30,28 @@ Provides-Extra: s3
30
30
  Requires-Dist: boto3<2,>=1.37; extra == "s3"
31
31
  Requires-Dist: botocore<2,>=1.37; extra == "s3"
32
32
  Provides-Extra: mongodb
33
- Requires-Dist: motor<4,>=3.7; extra == "mongodb"
33
+ Requires-Dist: pymongo<5,>=4.13; extra == "mongodb"
34
34
  Provides-Extra: opentelemetry-base
35
- Requires-Dist: opentelemetry-sdk<2,>=1.31.1; extra == "opentelemetry-base"
36
- Requires-Dist: opentelemetry-exporter-otlp<2,>=1.31.1; extra == "opentelemetry-base"
37
- Requires-Dist: opentelemetry-instrumentation>=0.52b1; extra == "opentelemetry-base"
38
- Requires-Dist: opentelemetry-instrumentation-httpx>=0.52b1; extra == "opentelemetry-base"
35
+ Requires-Dist: opentelemetry-sdk<2,>=1.35.0; extra == "opentelemetry-base"
36
+ Requires-Dist: opentelemetry-exporter-otlp<2,>=1.35.0; extra == "opentelemetry-base"
37
+ Requires-Dist: opentelemetry-instrumentation>=0.56b0; extra == "opentelemetry-base"
38
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.56b0; extra == "opentelemetry-base"
39
+ Requires-Dist: opentelemetry-distro>=0.56b0; extra == "opentelemetry-base"
39
40
  Provides-Extra: opentelemetry-akafka
40
41
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-akafka"
41
42
  Requires-Dist: hexkit[akafka]; extra == "opentelemetry-akafka"
42
- Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.52b1; extra == "opentelemetry-akafka"
43
+ Requires-Dist: opentelemetry-instrumentation-aiokafka>=0.56b0; extra == "opentelemetry-akafka"
43
44
  Provides-Extra: opentelemetry-mongodb
44
45
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-mongodb"
45
46
  Requires-Dist: hexkit[mongodb]; extra == "opentelemetry-mongodb"
46
- Requires-Dist: opentelemetry-instrumentation-pymongo>=0.52b1; extra == "opentelemetry-mongodb"
47
+ Requires-Dist: opentelemetry-instrumentation-pymongo>=0.56b0; extra == "opentelemetry-mongodb"
47
48
  Provides-Extra: opentelemetry-fastapi
48
49
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-fastapi"
49
- Requires-Dist: opentelemetry-instrumentation-fastapi>=0.52b1; extra == "opentelemetry-fastapi"
50
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.56b0; extra == "opentelemetry-fastapi"
50
51
  Provides-Extra: opentelemetry-s3
51
52
  Requires-Dist: hexkit[opentelemetry-base]; extra == "opentelemetry-s3"
52
53
  Requires-Dist: hexkit[s3]; extra == "opentelemetry-s3"
53
- Requires-Dist: opentelemetry-instrumentation-botocore>=0.52b1; extra == "opentelemetry-s3"
54
+ Requires-Dist: opentelemetry-instrumentation-botocore>=0.56b0; extra == "opentelemetry-s3"
54
55
  Provides-Extra: opentelemetry
55
56
  Requires-Dist: hexkit[opentelemetry-akafka]; extra == "opentelemetry"
56
57
  Requires-Dist: hexkit[opentelemetry-mongodb]; extra == "opentelemetry"
@@ -177,7 +178,7 @@ Additionally, the following convenience command is available inside the devconta
177
178
  (type it in the integrated terminal of VS Code):
178
179
  - `dev_install` - install the lib with all development dependencies and pre-commit hooks
179
180
  (please run that if you are starting the devcontainer for the first time
180
- or if added any python dependencies to the [`./setup.cfg`](./setup.cfg))
181
+ or if added any python dependencies to the [`./pyproject.toml`](./pyproject.toml))
181
182
 
182
183
  If you prefer not to use vscode, you could get a similar setup (without the editor
183
184
  specific features) by running the following commands:
@@ -103,7 +103,7 @@ Additionally, the following convenience command is available inside the devconta
103
103
  (type it in the integrated terminal of VS Code):
104
104
  - `dev_install` - install the lib with all development dependencies and pre-commit hooks
105
105
  (please run that if you are starting the devcontainer for the first time
106
- or if added any python dependencies to the [`./setup.cfg`](./setup.cfg))
106
+ or if added any python dependencies to the [`./pyproject.toml`](./pyproject.toml))
107
107
 
108
108
  If you prefer not to use vscode, you could get a similar setup (without the editor
109
109
  specific features) by running the following commands:
@@ -24,7 +24,7 @@ classifiers = [
24
24
  "Intended Audience :: Developers",
25
25
  ]
26
26
  name = "hexkit"
27
- version = "5.4.1"
27
+ version = "7.0.0"
28
28
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
29
29
  dependencies = [
30
30
  "opentelemetry-api >=1.31.1, <2",
@@ -46,32 +46,33 @@ s3 = [
46
46
  "botocore >=1.37, <2",
47
47
  ]
48
48
  mongodb = [
49
- "motor >=3.7, <4",
49
+ "pymongo >=4.13, <5",
50
50
  ]
51
51
  opentelemetry-base = [
52
- "opentelemetry-sdk >=1.31.1, <2",
53
- "opentelemetry-exporter-otlp >=1.31.1, <2",
54
- "opentelemetry-instrumentation >=0.52b1",
55
- "opentelemetry-instrumentation-httpx >=0.52b1",
52
+ "opentelemetry-sdk >=1.35.0, <2",
53
+ "opentelemetry-exporter-otlp >=1.35.0, <2",
54
+ "opentelemetry-instrumentation >=0.56b0",
55
+ "opentelemetry-instrumentation-httpx >=0.56b0",
56
+ "opentelemetry-distro >=0.56b0",
56
57
  ]
57
58
  opentelemetry-akafka = [
58
59
  "hexkit[opentelemetry-base]",
59
60
  "hexkit[akafka]",
60
- "opentelemetry-instrumentation-aiokafka >=0.52b1",
61
+ "opentelemetry-instrumentation-aiokafka >=0.56b0",
61
62
  ]
62
63
  opentelemetry-mongodb = [
63
64
  "hexkit[opentelemetry-base]",
64
65
  "hexkit[mongodb]",
65
- "opentelemetry-instrumentation-pymongo >=0.52b1",
66
+ "opentelemetry-instrumentation-pymongo >=0.56b0",
66
67
  ]
67
68
  opentelemetry-fastapi = [
68
69
  "hexkit[opentelemetry-base]",
69
- "opentelemetry-instrumentation-fastapi >=0.52b1",
70
+ "opentelemetry-instrumentation-fastapi >=0.56b0",
70
71
  ]
71
72
  opentelemetry-s3 = [
72
73
  "hexkit[opentelemetry-base]",
73
74
  "hexkit[s3]",
74
- "opentelemetry-instrumentation-botocore >=0.52b1",
75
+ "opentelemetry-instrumentation-botocore >=0.56b0",
75
76
  ]
76
77
  opentelemetry = [
77
78
  "hexkit[opentelemetry-akafka]",
@@ -102,9 +103,6 @@ all = [
102
103
  [project.urls]
103
104
  Repository = "https://github.com/ghga-de/hexkit"
104
105
 
105
- [project.entry-points.opentelemetry_distro]
106
- hexkit_distro = "hexkit.opentelemetry:HexkitDistro"
107
-
108
106
  [tool.setuptools.packages.find]
109
107
  where = [
110
108
  "src",
@@ -226,7 +224,7 @@ source = [
226
224
  [tool.tox]
227
225
  legacy_tox_ini = """
228
226
  [tox]
229
- env_list = py3{9,12,13}
227
+ env_list = py3{9,10,11,12,13}
230
228
 
231
229
  [gh-actions]
232
230
  python =
@@ -242,5 +240,7 @@ legacy_tox_ini = """
242
240
  DOCKER_HOST
243
241
  deps =
244
242
  --no-deps -r ./lock/requirements-dev.txt
245
- commands = pytest {posargs}
243
+ commands =
244
+ py3{9,10}: pip install --no-deps backports.asyncio.runner==1.2.0 exceptiongroup==1.3.0 tomli==2.2.1
245
+ pytest {posargs}
246
246
  """
@@ -17,19 +17,24 @@
17
17
  """Utilities related to correlation IDs"""
18
18
 
19
19
  import logging
20
+ from collections.abc import AsyncGenerator
20
21
  from contextlib import asynccontextmanager
21
22
  from contextvars import ContextVar
23
+ from typing import Any
22
24
  from uuid import UUID, uuid4
23
25
 
26
+ from pydantic import UUID4
27
+
24
28
  from hexkit.utils import set_context_var
25
29
 
26
30
  log = logging.getLogger(__name__)
27
31
 
28
- correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
32
+ correlation_id_var: ContextVar[UUID4] = ContextVar("correlation_id")
29
33
 
30
34
  __all__ = [
31
35
  "CorrelationIdContextError",
32
36
  "InvalidCorrelationIdError",
37
+ "correlation_id_from_str",
33
38
  "get_correlation_id",
34
39
  "new_correlation_id",
35
40
  "set_correlation_id",
@@ -42,36 +47,46 @@ class CorrelationIdContextError(RuntimeError):
42
47
  """Raised when the correlation ID ContextVar is unexpectedly not set."""
43
48
 
44
49
  def __init__(self):
45
- super().__init__("No correlation ID has been set.")
50
+ super().__init__("Correlation ID not set")
46
51
 
47
52
 
48
53
  class InvalidCorrelationIdError(RuntimeError):
49
54
  """Raised when a correlation ID fails validation."""
50
55
 
51
- def __init__(self, *, correlation_id: str):
52
- message = f"Invalid correlation ID found: '{correlation_id}'"
56
+ def __init__(self, *, correlation_id: Any):
57
+ message = f"Invalid correlation ID: {correlation_id!r}"
53
58
  super().__init__(message)
54
59
 
55
60
 
56
- def new_correlation_id() -> str:
61
+ def new_correlation_id() -> UUID4:
57
62
  """Generates a new correlation ID."""
58
- return str(uuid4())
63
+ return uuid4()
59
64
 
60
65
 
61
- def validate_correlation_id(correlation_id: str):
62
- """Raises an error if the correlation ID is invalid.
66
+ def validate_correlation_id(correlation_id: Any):
67
+ """Validate the correlation ID.
63
68
 
64
69
  Raises:
65
- InvalidCorrelationIdError: If the correlation ID is empty or invalid.
70
+ InvalidCorrelationIdError: If the correlation ID is not a UUID.
71
+ """
72
+ if not isinstance(correlation_id, UUID):
73
+ raise InvalidCorrelationIdError(correlation_id=correlation_id)
74
+
75
+
76
+ def correlation_id_from_str(correlation_id: str) -> UUID4:
77
+ """Convert a string to a UUID4.
78
+
79
+ Raises:
80
+ InvalidCorrelationIdError: If the string is not a valid UUID.
66
81
  """
67
82
  try:
68
- UUID(correlation_id)
83
+ return UUID(correlation_id)
69
84
  except ValueError as err:
70
85
  raise InvalidCorrelationIdError(correlation_id=correlation_id) from err
71
86
 
72
87
 
73
88
  @asynccontextmanager
74
- async def set_correlation_id(correlation_id: str):
89
+ async def set_correlation_id(correlation_id: UUID4):
75
90
  """Set the given correlation ID for the life of the context.
76
91
 
77
92
  Raises:
@@ -85,7 +100,7 @@ async def set_correlation_id(correlation_id: str):
85
100
 
86
101
 
87
102
  @asynccontextmanager
88
- async def set_new_correlation_id():
103
+ async def set_new_correlation_id() -> AsyncGenerator[UUID4, None]:
89
104
  """Set a new correlation ID for the life of the context."""
90
105
  correlation_id = new_correlation_id()
91
106
 
@@ -94,17 +109,19 @@ async def set_new_correlation_id():
94
109
  yield correlation_id
95
110
 
96
111
 
97
- def get_correlation_id() -> str:
112
+ def get_correlation_id() -> UUID4:
98
113
  """Get the correlation ID.
99
114
 
100
115
  This should only be called when the correlation ID ContextVar is expected to be set.
101
116
 
102
117
  Raises:
103
118
  CorrelationIdContextError: when the correlation ID ContextVar is not set.
104
- InvalidCorrelationIdError: when the correlation ID is invalid.
119
+ InvalidCorrelationIdError: when the correlation ID is set but invalid.
105
120
  """
106
- if not (correlation_id := correlation_id_var.get()):
107
- raise CorrelationIdContextError()
121
+ try:
122
+ correlation_id = correlation_id_var.get()
123
+ except LookupError as err:
124
+ raise CorrelationIdContextError() from err
108
125
 
109
126
  validate_correlation_id(correlation_id)
110
127
 
@@ -24,9 +24,7 @@ from typing import Any, Literal, Optional
24
24
  from pydantic import Field
25
25
  from pydantic_settings import BaseSettings
26
26
 
27
- from hexkit.correlation import (
28
- correlation_id_var,
29
- )
27
+ from hexkit.correlation import correlation_id_var
30
28
 
31
29
  __all__ = [
32
30
  "JsonFormatter",
@@ -0,0 +1,76 @@
1
+ # Copyright 2021 - 2025 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
2
+ # for the German Human Genome-Phenome Archive (GHGA)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """OpenTelemetry specific configuration code. This is gated behind the opentelemetry extra."""
16
+
17
+ import logging
18
+ import os
19
+ from typing import Annotated
20
+
21
+ from opentelemetry import trace
22
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
23
+ from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
24
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
25
+ from opentelemetry.sdk.trace import TracerProvider
26
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
27
+ from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
28
+ from pydantic import Field
29
+ from pydantic_settings import BaseSettings
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class OpenTelemetryConfig(BaseSettings):
35
+ """OpenTelemetry specific configuration options"""
36
+
37
+ enable_opentelemetry: bool = Field(
38
+ default=False,
39
+ description="If set to true, this will run necessary setup code."
40
+ "If set to false, environment variables are set that should also effectively "
41
+ "disable autoinstrumentation.",
42
+ )
43
+ otel_trace_sampling_rate: Annotated[float, Field(strict=True, ge=0, le=1)] = Field(
44
+ default=1.0,
45
+ description="Determines which proportion of spans should be sampled. "
46
+ "A value of 1.0 means all and is equivalent to the previous behaviour. "
47
+ "Setting this to 0 will result in no spans being sampled, but this does not "
48
+ "automatically set `enable_opentelemetry` to False.",
49
+ )
50
+
51
+
52
+ def configure_opentelemetry(*, service_name: str, config: OpenTelemetryConfig):
53
+ """Configure all needed parts of OpenTelemetry.
54
+
55
+ Setup of the TracerProvider is done programmatically, while disabling OpenTelemetry
56
+ sets the corresponding environment variable.
57
+ """
58
+ if config.enable_opentelemetry:
59
+ resource = Resource(attributes={SERVICE_NAME: service_name})
60
+ # Replace the default static sampler with a probabilistic one that honors parent
61
+ # span sampling decisions
62
+ # This should consistently yield full traces within a service but not necessarily
63
+ # across service boundaries
64
+ # With the default sampling rate, behaviour does not change, but this allows to
65
+ # introduce head sampling by adjusting a config option on the service side later on
66
+ sampler = ParentBasedTraceIdRatio(rate=config.otel_trace_sampling_rate)
67
+
68
+ # Initialize service specific TracerProvider
69
+ trace_provider = TracerProvider(resource=resource, sampler=sampler)
70
+ processor = BatchSpanProcessor(OTLPSpanExporter())
71
+ trace_provider.add_span_processor(processor)
72
+ trace.set_tracer_provider(trace_provider)
73
+ else:
74
+ # Currently OTEL_SDK_DISABLED doesn't seem to be honored by all implementations yet
75
+ # It seems to be working well enough for the Python implementation.
76
+ os.environ[OTEL_SDK_DISABLED] = "true"
@@ -19,6 +19,9 @@
19
19
  from abc import ABC, abstractmethod
20
20
  from collections.abc import Mapping
21
21
  from typing import Optional
22
+ from uuid import UUID, uuid4
23
+
24
+ from pydantic import UUID4
22
25
 
23
26
  from hexkit.custom_types import Ascii, JsonObject
24
27
  from hexkit.utils import check_ascii
@@ -27,13 +30,14 @@ from hexkit.utils import check_ascii
27
30
  class EventPublisherProtocol(ABC):
28
31
  """A protocol for publishing events to an event broker."""
29
32
 
30
- async def publish(
33
+ async def publish( # noqa: PLR0913
31
34
  self,
32
35
  *,
33
36
  payload: JsonObject,
34
37
  type_: Ascii,
35
38
  key: Ascii,
36
39
  topic: Ascii,
40
+ event_id: Optional[UUID4] = None,
37
41
  headers: Optional[Mapping[str, str]] = None,
38
42
  ) -> None:
39
43
  """Publish an event.
@@ -43,8 +47,16 @@ class EventPublisherProtocol(ABC):
43
47
  - `type_` (str): The event type. ASCII characters only.
44
48
  - `key` (str): The event type. ASCII characters only.
45
49
  - `topic` (str): The event type. ASCII characters only.
50
+ - `event_id` (UUID4, optional): An optional event ID. If not provided, a new
51
+ one will be generated.
46
52
  - `headers`: Additional headers to attach to the event.
47
53
  """
54
+ if event_id:
55
+ if not isinstance(event_id, UUID):
56
+ raise TypeError(f"event_id must be a UUID, got {type(event_id)}")
57
+ else:
58
+ event_id = uuid4()
59
+
48
60
  check_ascii(type_, key, topic)
49
61
  if headers is None:
50
62
  headers = {}
@@ -54,17 +66,19 @@ class EventPublisherProtocol(ABC):
54
66
  type_=type_,
55
67
  key=key,
56
68
  topic=topic,
69
+ event_id=event_id,
57
70
  headers=headers,
58
71
  )
59
72
 
60
73
  @abstractmethod
61
- async def _publish_validated(
74
+ async def _publish_validated( # noqa: PLR0913
62
75
  self,
63
76
  *,
64
77
  payload: JsonObject,
65
78
  type_: Ascii,
66
79
  key: Ascii,
67
80
  topic: Ascii,
81
+ event_id: UUID4,
68
82
  headers: Mapping[str, str],
69
83
  ) -> None:
70
84
  """Publish an event with already validated topic and type.
@@ -74,6 +88,7 @@ class EventPublisherProtocol(ABC):
74
88
  - `type_` (str): The event type. ASCII characters only.
75
89
  - `key` (str): The event type. ASCII characters only.
76
90
  - `topic` (str): The event type. ASCII characters only.
91
+ - `event_id` (UUID): The event ID.
77
92
  - `headers`: Additional headers to attach to the event.
78
93
  """
79
94
  ...
@@ -20,6 +20,8 @@ from abc import ABC, abstractmethod
20
20
  from collections.abc import Mapping
21
21
  from datetime import datetime
22
22
 
23
+ from pydantic import UUID4
24
+
23
25
  from hexkit.custom_types import Ascii, JsonObject
24
26
  from hexkit.utils import check_ascii
25
27
 
@@ -54,6 +56,7 @@ class EventSubscriberProtocol(ABC):
54
56
  type_: Ascii,
55
57
  topic: Ascii,
56
58
  key: Ascii,
59
+ event_id: UUID4,
57
60
  ) -> None:
58
61
  """Receive an event of interest and process it according to its type.
59
62
 
@@ -62,6 +65,7 @@ class EventSubscriberProtocol(ABC):
62
65
  type_: The type of the event.
63
66
  topic: Name of the topic the event was published to.
64
67
  key: A key used for routing the event.
68
+ event_id: The unique identifier of the event.
65
69
  """
66
70
  check_ascii(type_, topic)
67
71
 
@@ -69,7 +73,7 @@ class EventSubscriberProtocol(ABC):
69
73
  check_ascii(key)
70
74
 
71
75
  await self._consume_validated(
72
- payload=payload, type_=type_, topic=topic, key=key
76
+ payload=payload, type_=type_, topic=topic, key=key, event_id=event_id
73
77
  )
74
78
 
75
79
  @abstractmethod
@@ -80,6 +84,7 @@ class EventSubscriberProtocol(ABC):
80
84
  type_: Ascii,
81
85
  topic: Ascii,
82
86
  key: Ascii,
87
+ event_id: UUID4,
83
88
  ) -> None:
84
89
  """
85
90
  Receive and process an event with already validated topic, type, and key.
@@ -89,6 +94,7 @@ class EventSubscriberProtocol(ABC):
89
94
  type_: The type of the event.
90
95
  topic: Name of the topic the event was published to.
91
96
  key: A key used for routing the event.
97
+ event_id: The unique identifier of the event.
92
98
  """
93
99
  ...
94
100
 
@@ -114,6 +120,7 @@ class DLQSubscriberProtocol(ABC):
114
120
  type_: Ascii,
115
121
  topic: Ascii,
116
122
  key: Ascii,
123
+ event_id: UUID4,
117
124
  timestamp: datetime,
118
125
  headers: Mapping[str, str],
119
126
  ) -> None:
@@ -124,6 +131,7 @@ class DLQSubscriberProtocol(ABC):
124
131
  type_: The type of the event.
125
132
  topic: Name of the topic the event was published to.
126
133
  key: A key used for routing the event.
134
+ event_id: The unique identifier of the event.
127
135
  timestamp: The timestamp assigned by either the publisher or the broker.
128
136
  headers: The event headers containing important metadata.
129
137
  """
@@ -137,6 +145,7 @@ class DLQSubscriberProtocol(ABC):
137
145
  type_=type_,
138
146
  topic=topic,
139
147
  key=key,
148
+ event_id=event_id,
140
149
  timestamp=timestamp,
141
150
  headers=headers,
142
151
  )
@@ -149,6 +158,7 @@ class DLQSubscriberProtocol(ABC):
149
158
  type_: Ascii,
150
159
  topic: Ascii,
151
160
  key: Ascii,
161
+ event_id: UUID4,
152
162
  timestamp: datetime,
153
163
  headers: Mapping[str, str],
154
164
  ) -> None:
@@ -160,6 +170,7 @@ class DLQSubscriberProtocol(ABC):
160
170
  type_: The type of the event.
161
171
  topic: Name of the topic the event was published to.
162
172
  key: A key used for routing the event.
173
+ event_id: The unique identifier of the event.
163
174
  timestamp: The timestamp assigned by either the publisher or the broker.
164
175
  headers: The event headers containing important metadata.
165
176
  """
@@ -74,8 +74,8 @@ class KafkaConfig(BaseSettings):
74
74
  examples=[True, False],
75
75
  description=(
76
76
  "A flag, which, if False, will result in an error when trying to publish an"
77
- + " event without a valid correlation ID set for the context. If True, the a"
78
- + " newly correlation ID will be generated and used in the event header."
77
+ + " event without a valid correlation ID set for the context. If True, a"
78
+ + " new correlation ID will be generated and used in the event header."
79
79
  ),
80
80
  )
81
81
  kafka_max_message_size: PositiveInt = Field(
@@ -30,12 +30,12 @@ from typing import Any, Callable, Optional, Protocol
30
30
  from uuid import UUID
31
31
 
32
32
  from aiokafka import AIOKafkaProducer
33
+ from pydantic import UUID4
33
34
 
34
35
  from hexkit.correlation import (
35
36
  CorrelationIdContextError,
36
37
  get_correlation_id,
37
38
  new_correlation_id,
38
- validate_correlation_id,
39
39
  )
40
40
  from hexkit.custom_types import Ascii, JsonObject, KafkaCompressionType
41
41
  from hexkit.protocols.eventpub import EventPublisherProtocol
@@ -45,7 +45,7 @@ from hexkit.providers.akafka.provider.utils import (
45
45
  generate_ssl_context,
46
46
  )
47
47
 
48
- RESERVED_HEADERS = ["type", "correlation_id"]
48
+ RESERVED_HEADERS = ["type", "correlation_id", "event_id"]
49
49
 
50
50
 
51
51
  class KafkaProducerCompatible(Protocol):
@@ -171,13 +171,14 @@ class KafkaEventPublisher(EventPublisherProtocol):
171
171
  return value.isoformat()
172
172
  raise TypeError(f"Object of type {type(value)} is not JSON serializable")
173
173
 
174
- async def _publish_validated(
174
+ async def _publish_validated( # noqa: PLR0913
175
175
  self,
176
176
  *,
177
177
  payload: JsonObject,
178
178
  type_: Ascii,
179
179
  key: Ascii,
180
180
  topic: Ascii,
181
+ event_id: UUID4,
181
182
  headers: Mapping[str, str],
182
183
  ) -> None:
183
184
  """Publish an event with already validated topic and type.
@@ -187,6 +188,7 @@ class KafkaEventPublisher(EventPublisherProtocol):
187
188
  - `type_` (str): The event type. ASCII characters only.
188
189
  - `key` (str): The event key. ASCII characters only.
189
190
  - `topic` (str): The event topic. ASCII characters only.
191
+ - `event_id` (UUID): The event ID.
190
192
  - `headers`: Additional headers to attach to the event.
191
193
  """
192
194
  try:
@@ -196,25 +198,37 @@ class KafkaEventPublisher(EventPublisherProtocol):
196
198
  raise
197
199
 
198
200
  correlation_id = new_correlation_id()
199
- logging.info("Generated new correlation ID: %s", correlation_id)
200
-
201
- validate_correlation_id(correlation_id)
201
+ logging.info("Generated new correlation ID: %s.", correlation_id)
202
202
 
203
203
  # Create a shallow copy of the headers
204
- headers_copy = dict(headers)
204
+ headers_copy = dict(headers) # used for validation and logging only
205
205
 
206
- # Check and log warnings for reserved headers
207
- for header in RESERVED_HEADERS:
208
- log_msg = (
209
- f"The '{header}' header shouldn't be supplied, but was. Overwriting."
210
- )
211
- if header in headers_copy:
212
- logging.warning(log_msg, extra={header: headers_copy[header]})
206
+ overrides = {
207
+ header: headers_copy[header]
208
+ for header in RESERVED_HEADERS
209
+ if header in headers_copy
210
+ }
213
211
 
214
212
  headers_copy["type"] = type_
215
- headers_copy["correlation_id"] = correlation_id
213
+ headers_copy["event_id"] = str(event_id)
214
+ headers_copy["correlation_id"] = str(correlation_id)
216
215
  encoded_headers_list = [(k, v.encode("ascii")) for k, v in headers_copy.items()]
217
216
 
217
+ # Log warnings for any reserved headers that were overridden
218
+ for header, old_value in overrides.items():
219
+ log_msg = (
220
+ f"The '{header}' header shouldn't be supplied, but was."
221
+ f" Overwriting old value ({old_value}) with {headers_copy[header]}."
222
+ )
223
+ logging.warning(log_msg, extra={header: old_value})
224
+
218
225
  await self._producer.send_and_wait(
219
226
  topic, key=key, value=payload, headers=encoded_headers_list
220
227
  )
228
+ logging.info(
229
+ "Published event: topic=%s, type=%s, key=%s, event_id=%s.",
230
+ topic,
231
+ type_,
232
+ key,
233
+ event_id,
234
+ )