|
| 1 | +import pytest |
| 2 | + |
| 3 | +from posthog.tracing._config import ( |
| 4 | + DEFAULT_FLUSH_INTERVAL_SECONDS, |
| 5 | + DEFAULT_MAX_EXPORT_BATCH_SIZE, |
| 6 | + DEFAULT_MAX_LIVE_SPANS, |
| 7 | + DEFAULT_MAX_QUEUE_SIZE, |
| 8 | + DEFAULT_MAX_SPAN_AGE_SECONDS, |
| 9 | + ResolvedTracesConfig, |
| 10 | + resolve_traces_config, |
| 11 | +) |
| 12 | + |
| 13 | + |
| 14 | +class TestDefaults: |
| 15 | + def test_applies_the_documented_defaults(self): |
| 16 | + assert resolve_traces_config({}) == ResolvedTracesConfig( |
| 17 | + flush_interval=DEFAULT_FLUSH_INTERVAL_SECONDS, |
| 18 | + max_export_batch_size=DEFAULT_MAX_EXPORT_BATCH_SIZE, |
| 19 | + max_queue_size=DEFAULT_MAX_QUEUE_SIZE, |
| 20 | + max_live_spans=DEFAULT_MAX_LIVE_SPANS, |
| 21 | + max_span_age=DEFAULT_MAX_SPAN_AGE_SECONDS, |
| 22 | + ) |
| 23 | + |
| 24 | + def test_leaves_service_name_unset_so_the_encoder_supplies_unknown_service(self): |
| 25 | + assert resolve_traces_config({}).service_name is None |
| 26 | + |
| 27 | + @pytest.mark.parametrize("config", [None, "nope", 42, ["a"]]) |
| 28 | + def test_a_non_dict_config_falls_back_to_defaults(self, config): |
| 29 | + assert resolve_traces_config(config) == resolve_traces_config({}) |
| 30 | + |
| 31 | + |
| 32 | +class TestExplicitValues: |
| 33 | + def test_honours_explicit_values(self): |
| 34 | + resolved = resolve_traces_config( |
| 35 | + { |
| 36 | + "service_name": "api", |
| 37 | + "service_version": "1.2.3", |
| 38 | + "environment": "prod", |
| 39 | + "flush_interval": 2, |
| 40 | + "max_export_batch_size": 100, |
| 41 | + "max_queue_size": 400, |
| 42 | + "max_live_spans": 50, |
| 43 | + "max_span_age": 60, |
| 44 | + } |
| 45 | + ) |
| 46 | + assert resolved == ResolvedTracesConfig( |
| 47 | + service_name="api", |
| 48 | + service_version="1.2.3", |
| 49 | + environment="prod", |
| 50 | + flush_interval=2.0, |
| 51 | + max_export_batch_size=100, |
| 52 | + max_queue_size=400, |
| 53 | + max_live_spans=50, |
| 54 | + max_span_age=60.0, |
| 55 | + ) |
| 56 | + |
| 57 | + @pytest.mark.parametrize( |
| 58 | + "value", |
| 59 | + [0, -1, 0.5, 512.7, float("nan"), float("inf"), "512", True, None], |
| 60 | + ) |
| 61 | + def test_falls_back_for_an_unusable_batch_size(self, value): |
| 62 | + assert ( |
| 63 | + resolve_traces_config( |
| 64 | + {"max_export_batch_size": value} |
| 65 | + ).max_export_batch_size |
| 66 | + == DEFAULT_MAX_EXPORT_BATCH_SIZE |
| 67 | + ) |
| 68 | + |
| 69 | + def test_accepts_a_whole_number_float_batch_size(self): |
| 70 | + assert ( |
| 71 | + resolve_traces_config( |
| 72 | + {"max_export_batch_size": 100.0} |
| 73 | + ).max_export_batch_size |
| 74 | + == 100 |
| 75 | + ) |
| 76 | + |
| 77 | + def test_an_unusable_knob_keeps_the_rest_of_the_config(self): |
| 78 | + resolved = resolve_traces_config( |
| 79 | + {"service_name": "api", "max_live_spans": float("inf")} |
| 80 | + ) |
| 81 | + assert resolved.service_name == "api" |
| 82 | + assert resolved.max_live_spans == DEFAULT_MAX_LIVE_SPANS |
| 83 | + |
| 84 | + @pytest.mark.parametrize("value", [0, -1, float("nan"), float("inf"), "5", False]) |
| 85 | + def test_falls_back_for_an_unusable_flush_interval(self, value): |
| 86 | + assert ( |
| 87 | + resolve_traces_config({"flush_interval": value}).flush_interval |
| 88 | + == DEFAULT_FLUSH_INTERVAL_SECONDS |
| 89 | + ) |
| 90 | + |
| 91 | + @pytest.mark.parametrize("value", [0, -1, float("nan"), float("inf")]) |
| 92 | + def test_falls_back_for_unusable_live_span_bounds(self, value): |
| 93 | + resolved = resolve_traces_config( |
| 94 | + {"max_live_spans": value, "max_span_age": value} |
| 95 | + ) |
| 96 | + assert resolved.max_live_spans == DEFAULT_MAX_LIVE_SPANS |
| 97 | + assert resolved.max_span_age == DEFAULT_MAX_SPAN_AGE_SECONDS |
| 98 | + |
| 99 | + def test_keeps_the_queue_at_least_as_large_as_the_export_batch(self): |
| 100 | + resolved = resolve_traces_config({"max_export_batch_size": 4096}) |
| 101 | + assert resolved.max_queue_size == 4096 |
| 102 | + |
| 103 | + def test_floors_an_explicit_queue_size_at_the_batch_size(self): |
| 104 | + resolved = resolve_traces_config( |
| 105 | + {"max_export_batch_size": 10, "max_queue_size": 3} |
| 106 | + ) |
| 107 | + assert resolved.max_queue_size == 10 |
| 108 | + |
| 109 | + def test_ignores_a_non_string_named_field(self): |
| 110 | + assert resolve_traces_config({"service_name": 42}).service_name is None |
| 111 | + |
| 112 | + |
| 113 | +class TestResourceAttributes: |
| 114 | + def test_lets_otlp_resource_attributes_override_the_named_fields(self): |
| 115 | + resolved = resolve_traces_config( |
| 116 | + { |
| 117 | + "service_name": "named", |
| 118 | + "resource_attributes": {"service.name": "from-attrs", "region": "eu"}, |
| 119 | + } |
| 120 | + ) |
| 121 | + assert resolved.service_name == "from-attrs" |
| 122 | + assert resolved.resource_attributes == { |
| 123 | + "service.name": "from-attrs", |
| 124 | + "region": "eu", |
| 125 | + } |
| 126 | + |
| 127 | + def test_attaches_host_attributes_and_lets_user_attributes_override_them(self): |
| 128 | + resolved = resolve_traces_config( |
| 129 | + {"resource_attributes": {"os.name": "Custom"}}, |
| 130 | + {"os.name": "Linux", "os.version": "6.1"}, |
| 131 | + ) |
| 132 | + assert resolved.resource_attributes == { |
| 133 | + "os.name": "Custom", |
| 134 | + "os.version": "6.1", |
| 135 | + } |
| 136 | + |
| 137 | + def test_ignores_a_non_dict_value(self): |
| 138 | + assert ( |
| 139 | + resolve_traces_config({"resource_attributes": ["a"]}).resource_attributes |
| 140 | + == {} |
| 141 | + ) |
| 142 | + |
| 143 | + def test_drops_an_identity_key_that_is_not_a_string(self): |
| 144 | + resolved = resolve_traces_config( |
| 145 | + { |
| 146 | + "service_name": "named", |
| 147 | + "resource_attributes": { |
| 148 | + "service.name": 42, |
| 149 | + "deployment.environment": 1, |
| 150 | + }, |
| 151 | + } |
| 152 | + ) |
| 153 | + assert resolved.service_name == "named" |
| 154 | + assert resolved.environment is None |
| 155 | + assert "service.name" not in resolved.resource_attributes |
| 156 | + |
| 157 | + def test_keeps_the_readable_attributes_when_one_accessor_raises(self): |
| 158 | + class Explosive(dict): |
| 159 | + def __getitem__(self, key): |
| 160 | + if key == "bad": |
| 161 | + raise RuntimeError("boom") |
| 162 | + return super().__getitem__(key) |
| 163 | + |
| 164 | + resolved = resolve_traces_config( |
| 165 | + {"resource_attributes": Explosive(good=1, bad=2)} |
| 166 | + ) |
| 167 | + assert resolved.resource_attributes == {"good": 1} |
| 168 | + |
| 169 | + |
| 170 | +class TestHostileResourceAttributeKeys: |
| 171 | + def test_drops_only_a_key_that_cannot_be_stringified(self): |
| 172 | + class HostileKey: |
| 173 | + def __str__(self): |
| 174 | + raise RuntimeError("no") |
| 175 | + |
| 176 | + resolved = resolve_traces_config( |
| 177 | + { |
| 178 | + "service_name": "api", |
| 179 | + "resource_attributes": {HostileKey(): 1, "team": "x"}, |
| 180 | + } |
| 181 | + ) |
| 182 | + assert resolved.service_name == "api" |
| 183 | + assert resolved.resource_attributes["team"] == "x" |
| 184 | + assert all(isinstance(key, str) for key in resolved.resource_attributes) |
0 commit comments