hexkit 4.4.0__tar.gz → 4.6.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 (64) hide show
  1. {hexkit-4.4.0/src/hexkit.egg-info → hexkit-4.6.0}/PKG-INFO +1 -1
  2. {hexkit-4.4.0 → hexkit-4.6.0}/pyproject.toml +2 -2
  3. hexkit-4.6.0/src/hexkit/opentelemetry_setup.py +170 -0
  4. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/testutils.py +117 -16
  5. {hexkit-4.4.0 → hexkit-4.6.0/src/hexkit.egg-info}/PKG-INFO +1 -1
  6. hexkit-4.4.0/src/hexkit/opentelemetry_setup.py +0 -30
  7. {hexkit-4.4.0 → hexkit-4.6.0}/LICENSE +0 -0
  8. {hexkit-4.4.0 → hexkit-4.6.0}/MANIFEST.in +0 -0
  9. {hexkit-4.4.0 → hexkit-4.6.0}/README.md +0 -0
  10. {hexkit-4.4.0 → hexkit-4.6.0}/setup.cfg +0 -0
  11. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/__init__.py +0 -0
  12. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/__main__.py +0 -0
  13. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/base.py +0 -0
  14. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/config.py +0 -0
  15. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/correlation.py +0 -0
  16. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/custom_types.py +0 -0
  17. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/log.py +0 -0
  18. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/protocols/__init__.py +0 -0
  19. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/protocols/dao.py +0 -0
  20. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/protocols/daopub.py +0 -0
  21. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/protocols/daosub.py +0 -0
  22. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/protocols/eventpub.py +0 -0
  23. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/protocols/eventsub.py +0 -0
  24. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/protocols/objstorage.py +0 -0
  25. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/__init__.py +0 -0
  26. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/__init__.py +0 -0
  27. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/config.py +0 -0
  28. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/provider/__init__.py +0 -0
  29. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/provider/daosub.py +0 -0
  30. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/provider/eventpub.py +0 -0
  31. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/provider/eventsub.py +0 -0
  32. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/provider/utils.py +0 -0
  33. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/akafka/testcontainer.py +0 -0
  34. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongodb/__init__.py +0 -0
  35. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongodb/migrations/__init__.py +0 -0
  36. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongodb/migrations/_manager.py +0 -0
  37. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongodb/migrations/_utils.py +0 -0
  38. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongodb/provider.py +0 -0
  39. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongodb/testutils.py +0 -0
  40. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongokafka/__init__.py +0 -0
  41. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongokafka/provider/__init__.py +0 -0
  42. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongokafka/provider/config.py +0 -0
  43. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongokafka/provider/daopub.py +0 -0
  44. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongokafka/provider/persistent_pub.py +0 -0
  45. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/mongokafka/testutils.py +0 -0
  46. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/__init__.py +0 -0
  47. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/provider.py +0 -0
  48. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/test_files/__init__.py +0 -0
  49. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/test_files/test_file1.yaml +0 -0
  50. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/test_files/test_file2.yaml +0 -0
  51. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/test_files/test_file3.yaml +0 -0
  52. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/test_files/test_file4.yaml +0 -0
  53. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/testutils/__init__.py +0 -0
  54. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/testutils/_fixtures.py +0 -0
  55. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/testutils/_typical_workflow.py +0 -0
  56. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/s3/testutils/_utils.py +0 -0
  57. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/testing/__init__.py +0 -0
  58. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/providers/testing/eventpub.py +0 -0
  59. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/py.typed +0 -0
  60. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit/utils.py +0 -0
  61. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit.egg-info/SOURCES.txt +0 -0
  62. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit.egg-info/dependency_links.txt +0 -0
  63. {hexkit-4.4.0 → hexkit-4.6.0}/src/hexkit.egg-info/requires.txt +0 -0
  64. {hexkit-4.4.0 → hexkit-4.6.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: 4.4.0
3
+ Version: 4.6.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
@@ -1,6 +1,6 @@
1
1
  [build-system]
2
2
  requires = [
3
- "setuptools>=78.1",
3
+ "setuptools>=80.3",
4
4
  ]
5
5
  build-backend = "setuptools.build_meta"
6
6
 
@@ -24,7 +24,7 @@ classifiers = [
24
24
  "Intended Audience :: Developers",
25
25
  ]
26
26
  name = "hexkit"
27
- version = "4.4.0"
27
+ version = "4.6.0"
28
28
  description = "A Toolkit for Building Microservices using the Hexagonal Architecture"
29
29
  dependencies = [
30
30
  "opentelemetry-api >=1.31.1, <2",
@@ -0,0 +1,170 @@
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, Callable, Literal, Optional
20
+
21
+ from opentelemetry import trace
22
+ from opentelemetry.environment_variables import (
23
+ OTEL_LOGS_EXPORTER,
24
+ OTEL_METRICS_EXPORTER,
25
+ OTEL_TRACES_EXPORTER,
26
+ )
27
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
28
+ from opentelemetry.sdk.environment_variables import (
29
+ OTEL_EXPORTER_OTLP_PROTOCOL,
30
+ OTEL_SDK_DISABLED,
31
+ )
32
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
33
+ from opentelemetry.sdk.trace import TracerProvider
34
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
35
+ from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
36
+ from pydantic import Field
37
+ from pydantic_settings import BaseSettings
38
+ from typing_extensions import deprecated
39
+
40
+ TRACER: Optional["SpanTracer"] = None
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+
45
+ class SpanTracer:
46
+ """Custom tracer class providing a decorator to autpopulate span names."""
47
+
48
+ def __init__(self, name):
49
+ self.tracer = trace.get_tracer(name)
50
+
51
+ def start_span(
52
+ self, *, record_exception: bool = False, set_status_on_exception: bool = False
53
+ ):
54
+ """Decorator function starting a span populated with the __qualname__ of the wrapped function"""
55
+
56
+ def outer_wrapper(function: Callable):
57
+ def traced_function(*args, **kwargs):
58
+ name = function.__qualname__
59
+ with self.tracer.start_as_current_span(
60
+ name,
61
+ record_exception=record_exception,
62
+ set_status_on_exception=set_status_on_exception,
63
+ ):
64
+ return function(*args, **kwargs)
65
+
66
+ return traced_function
67
+
68
+ return outer_wrapper
69
+
70
+
71
+ class OpenTelemetryConfig(BaseSettings):
72
+ """OpenTelemetry specific configuration options"""
73
+
74
+ enable_opentelemetry: bool = Field(
75
+ default=False,
76
+ description="If set to true, this will run necessary setup code."
77
+ "If set to false, environment variables are set that should also effectively "
78
+ "disable autoinstrumentation.",
79
+ )
80
+ samplig_rate: Annotated[float, Field(strict=True, ge=0, le=1)] = Field(
81
+ default=1.0,
82
+ description="Determines which proportion of spans should be sampled. "
83
+ "A value of 1.0 means all and is equivalent to the previous behaviour. "
84
+ "Setting this to 0 will result in no spans being sampled, but this does not "
85
+ "automatically set `enable_opentelemetry` to False.",
86
+ )
87
+ protocol: Literal["grpc", "http/protobuf"] = Field(
88
+ default="http/protobuf",
89
+ description="Specifies which protocol should be used by exporters.",
90
+ )
91
+
92
+
93
+ def configure_opentelemetry(*, service_name: str, config: OpenTelemetryConfig):
94
+ """Configure all needed parts of OpenTelemetry.
95
+
96
+ Setup of the TracerProvider is done programmatically, all other configuration exports
97
+ OpenTelemetry specific environment variables.
98
+ """
99
+ configure_tracer(service_name=service_name, config=config)
100
+
101
+
102
+ @deprecated(
103
+ "This function will be removed in v5. Use `configure_opentelemetry instead.`"
104
+ )
105
+ def configure_tracer(*, service_name: str, config: OpenTelemetryConfig):
106
+ """Configure all needed parts of OpenTelemetry.
107
+
108
+ Setup of the TracerProvider is done programmatically, all other configuration exports
109
+ OpenTelemetry specific environment variables.
110
+ """
111
+ global TRACER
112
+ # opentelemetry distro sets this to grpc, but in the current context http/protobuf is preferred
113
+ os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] = config.protocol
114
+ # Disable OpenTelemetry metrics and logs explicitly as they are not processed in the backend currently
115
+ # This overwrites the defaults of `otlp` set in opentelemetry distro
116
+ os.environ[OTEL_METRICS_EXPORTER] = "none"
117
+ os.environ[OTEL_LOGS_EXPORTER] = "none"
118
+
119
+ if config.enable_opentelemetry:
120
+ if TRACER is not None:
121
+ logger.warning(
122
+ "OpenTelemetry configuration code should only be run once. "
123
+ "If it has been run with a different service name than %s before, "
124
+ "the tracer and resource name will likely be wrong in some cases.",
125
+ service_name,
126
+ )
127
+ resource = Resource(attributes={SERVICE_NAME: service_name})
128
+ # Replace the default static sampler with a probabilistic one that honors parent
129
+ # span sampling decisions
130
+ # This should consistently yield full traces within a service but not necessarily
131
+ # across service boundaries
132
+ # With the default sampling rate, behaviour does not change, but this allows to
133
+ # introduce head sampling by adjusting a config option on the service side later on
134
+ sampler = ParentBasedTraceIdRatio(rate=config.samplig_rate)
135
+
136
+ # Initialize service specific TracerProvider
137
+ trace_provider = TracerProvider(resource=resource, sampler=sampler)
138
+ processor = BatchSpanProcessor(OTLPSpanExporter())
139
+ trace_provider.add_span_processor(processor)
140
+ trace.set_tracer_provider(trace_provider)
141
+ TRACER = SpanTracer(service_name)
142
+ else:
143
+ # Currently OTEL_SDK_DISABLED doesn't seem to be honored by all implementations yet
144
+ # It seems to be working well enough for the Python implementation, but to be on
145
+ # the safe side, let's explicitly disable the trace exporter for now
146
+ os.environ[OTEL_TRACES_EXPORTER] = "none"
147
+ os.environ[OTEL_SDK_DISABLED] = "true"
148
+
149
+
150
+ def start_span(
151
+ *,
152
+ record_exception: bool = False,
153
+ set_status_on_exception: bool = False,
154
+ ) -> Callable:
155
+ """Returns decorated or undecorated function depending on if TRACER is instantiated.
156
+
157
+ Should be used as a decorator.
158
+ """
159
+
160
+ def wrapper(function: Callable):
161
+ # Caller did not have any time to populate from config yet or otel is disabled
162
+ if TRACER is None:
163
+ return function
164
+ # Return decorated function
165
+ return TRACER.start_span(
166
+ record_exception=record_exception,
167
+ set_status_on_exception=set_status_on_exception,
168
+ )(function)
169
+
170
+ return wrapper
@@ -19,12 +19,15 @@
19
19
  Please note, only use for testing purposes.
20
20
  """
21
21
 
22
+ import asyncio
22
23
  import json
23
24
  from collections.abc import AsyncGenerator, Generator, Mapping, Sequence
24
25
  from contextlib import asynccontextmanager
25
26
  from dataclasses import dataclass
26
27
  from functools import partial
27
- from typing import Any, Optional, Union
28
+ from typing import Any, Optional, Union, cast
29
+
30
+ from aiokafka.admin.config_resource import ConfigResource, ConfigResourceType
28
31
 
29
32
  try:
30
33
  from typing import Self
@@ -411,6 +414,7 @@ class KafkaFixture:
411
414
  self.config = config
412
415
  self.kafka_servers = kafka_servers
413
416
  self.publisher = publisher
417
+ self.admin_client: Optional[AIOKafkaAdminClient] = None
414
418
 
415
419
  async def publish_event(
416
420
  self,
@@ -442,6 +446,80 @@ class KafkaFixture:
442
446
  capture_headers=capture_headers,
443
447
  )
444
448
 
449
+ @asynccontextmanager
450
+ async def get_admin_client(self):
451
+ """An async context manager that provides a running AIOKafkaAdminClient
452
+ as a singleton instance.
453
+
454
+ If a running client already exists, it is yielded directly.
455
+ If not, this is considered the root use of the CM, so we start a new client,
456
+ yield it, then close the client within a `finally` block.
457
+ """
458
+ if self.admin_client:
459
+ yield self.admin_client
460
+ return
461
+
462
+ # No running client exists, so create one and make sure to clean up afterward.
463
+ self.admin_client = AIOKafkaAdminClient(bootstrap_servers=self.kafka_servers)
464
+ await self.admin_client.start()
465
+ try:
466
+ yield self.admin_client
467
+ finally:
468
+ await self.admin_client.close()
469
+ self.admin_client = None
470
+
471
+ async def get_cleanup_policy(self, *, topic: str) -> Optional[str]:
472
+ """Get the current cleanup policy for a topic or None if the topic doesn't exist"""
473
+ return await self.get_topic_config(topic=topic, config_name="cleanup.policy")
474
+
475
+ async def get_topic_config(self, *, topic: str, config_name: str) -> Optional[str]:
476
+ """Get the current config for a topic or None if the topic/config doesn't exist"""
477
+ async with self.get_admin_client() as admin_client:
478
+ # fetch a list containing a response for each requested topic
479
+ config_list = await admin_client.describe_configs(
480
+ [ConfigResource(ConfigResourceType.TOPIC, name=topic)]
481
+ )
482
+
483
+ # We only requested one topic, so get the first/only element from the list
484
+ config_response = config_list[0]
485
+
486
+ # Each config response has a 'resources' attribute
487
+ resources = config_response.resources
488
+
489
+ # The resources attr is a list containing one tuple for each requested topic.
490
+ # We only requested one topic, so our target is the first element.
491
+ requested_resource = resources[0]
492
+
493
+ # The resource itself is a list, and the topic config is at the end
494
+ configs = requested_resource[-1]
495
+ value = None
496
+ config_name = config_name.lower()
497
+
498
+ # The `configs` item is a list of tuples, where each tuple is a config item
499
+ # The first item is the config name, the second is the value, and the rest are ancillary
500
+ for item in configs:
501
+ if item[0].lower() == config_name:
502
+ value = item[1]
503
+ break
504
+ return value
505
+
506
+ async def set_cleanup_policy(self, *, topic: str, policy: str):
507
+ """Set the cleanup policy for the given topic. The topic must already exist."""
508
+ await self.set_topic_config(topic=topic, config={"cleanup.policy": policy})
509
+
510
+ async def set_topic_config(self, *, topic: str, config: dict[str, Any]):
511
+ """Set an arbitrary config value(s) for a topic"""
512
+ async with self.get_admin_client() as admin_client:
513
+ await admin_client.alter_configs(
514
+ config_resources=[
515
+ ConfigResource(
516
+ ConfigResourceType.TOPIC,
517
+ name=topic,
518
+ configs=config,
519
+ )
520
+ ]
521
+ )
522
+
445
523
  async def clear_topics(
446
524
  self,
447
525
  *,
@@ -453,26 +531,49 @@ class KafkaFixture:
453
531
  If no topics are specified, all topics will be cleared, except internal topics
454
532
  unless otherwise specified.
455
533
  """
456
- admin_client = AIOKafkaAdminClient(bootstrap_servers=self.kafka_servers)
457
- await admin_client.start()
458
- try:
459
- if topics is None:
460
- topics = await admin_client.list_topics()
534
+ async with self.get_admin_client() as admin_client:
535
+ available_topics = await admin_client.list_topics()
536
+ if not topics:
537
+ topics = available_topics
461
538
  elif isinstance(topics, str):
462
539
  topics = [topics]
540
+ topics = cast(list, topics)
463
541
  if exclude_internal:
464
542
  topics = [topic for topic in topics if not topic.startswith("__")]
543
+
544
+ # Filter out any topics that don't exist
545
+ topics = [topic for topic in topics if topic in available_topics]
546
+ if not topics:
547
+ return
548
+
549
+ # Record the current cleanup policies before modifying them
465
550
  topics_info = await admin_client.describe_topics(topics)
466
- records_to_delete = {
467
- TopicPartition(
468
- topic=topic_info["topic"], partition=partition_info["partition"]
469
- ): RecordsToDelete(before_offset=-1)
470
- for topic_info in topics_info
471
- for partition_info in topic_info["partitions"]
472
- }
473
- await admin_client.delete_records(records_to_delete, timeout_ms=10000)
474
- finally:
475
- await admin_client.close()
551
+ for topic_info in topics_info:
552
+ topic = topic_info["topic"]
553
+ original_policy = await self.get_cleanup_policy(topic=topic)
554
+ if original_policy and "compact" in original_policy.split(","):
555
+ await self.set_cleanup_policy(topic=topic, policy="delete")
556
+ await asyncio.sleep(0.2) # brief pause to wait for policy changes
557
+
558
+ records_to_delete = {
559
+ TopicPartition(
560
+ topic=topic, partition=partition_info["partition"]
561
+ ): RecordsToDelete(before_offset=-1)
562
+ for partition_info in topic_info["partitions"]
563
+ }
564
+
565
+ # Delete any events in the topic
566
+ try:
567
+ await admin_client.delete_records(
568
+ records_to_delete, timeout_ms=10000
569
+ )
570
+ finally:
571
+ # Always set compacted topics back to their original policy
572
+ if original_policy:
573
+ await self.set_cleanup_policy(
574
+ topic=topic,
575
+ policy=original_policy,
576
+ )
476
577
 
477
578
  @asynccontextmanager
478
579
  async def expect_events(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexkit
3
- Version: 4.4.0
3
+ Version: 4.6.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
@@ -1,30 +0,0 @@
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
- from opentelemetry import trace
18
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
19
- from opentelemetry.sdk.resources import SERVICE_NAME, Resource
20
- from opentelemetry.sdk.trace import TracerProvider
21
- from opentelemetry.sdk.trace.export import BatchSpanProcessor
22
-
23
-
24
- def configure_tracer(service_name: str):
25
- """Set up a global tracer for a specific service."""
26
- resource = Resource(attributes={SERVICE_NAME: service_name})
27
- trace_provider = TracerProvider(resource=resource)
28
- processor = BatchSpanProcessor(OTLPSpanExporter())
29
- trace_provider.add_span_processor(processor)
30
- trace.set_tracer_provider(trace_provider)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes