src-py-lib 0.1.9__tar.gz → 0.2.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.
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/.github/workflows/release.yml +32 -7
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/.github/workflows/validate.yml +2 -2
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/PKG-INFO +5 -1
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/pyproject.toml +9 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/__init__.py +41 -19
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/graphql.py +2 -2
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/sourcegraph.py +35 -29
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/utils/http.py +34 -2
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/utils/logging.py +108 -216
- src_py_lib-0.2.0/src/src_py_lib/utils/telemetry.py +494 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/tests/test_import.py +6 -1
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/tests/test_logging_http_clients.py +27 -27
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/uv.lock +236 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/.github/CODEOWNERS +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/.github/workflows/ci.yml +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/.gitignore +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/.markdownlint-cli2.yaml +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/.python-version +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/AGENTS.md +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/LICENSE +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/README.md +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/SECURITY.md +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/renovate.json +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/__init__.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/github.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/google_sheets.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/linear.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/one_password.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/clients/slack.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/py.typed +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/utils/__init__.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/utils/config.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/utils/json_cache.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/utils/json_types.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/src/src_py_lib/utils/tsv.py +0 -0
- {src_py_lib-0.1.9 → src_py_lib-0.2.0}/tests/test_tsv.py +0 -0
|
@@ -106,15 +106,34 @@ jobs:
|
|
|
106
106
|
tag_revision="$(git rev-list -n 1 "${release_tag}")"
|
|
107
107
|
git fetch --no-tags origin main
|
|
108
108
|
main_revision="$(git rev-parse origin/main)"
|
|
109
|
-
if
|
|
110
|
-
echo "::error title=Tag is not
|
|
111
|
-
echo "::error::Tag
|
|
109
|
+
if ! git merge-base --is-ancestor "${tag_revision}" "${main_revision}"; then
|
|
110
|
+
echo "::error title=Tag is not on main::Tag '${release_tag}' points at ${tag_revision}, which is not reachable from origin/main."
|
|
111
|
+
echo "::error::Tag a commit from main, then rerun the release."
|
|
112
112
|
exit 1
|
|
113
113
|
fi
|
|
114
114
|
|
|
115
115
|
echo "tag=${release_tag}" >> "${GITHUB_OUTPUT}"
|
|
116
116
|
echo "version=${release_tag#v}" >> "${GITHUB_OUTPUT}"
|
|
117
117
|
|
|
118
|
+
- name: Validate runtime dependencies
|
|
119
|
+
run: |
|
|
120
|
+
runtime_requirements_file="build/release/runtime-requirements.txt"
|
|
121
|
+
mkdir -p "$(dirname "${runtime_requirements_file}")"
|
|
122
|
+
uv export \
|
|
123
|
+
--no-dev \
|
|
124
|
+
--no-emit-project \
|
|
125
|
+
--no-hashes \
|
|
126
|
+
--no-header \
|
|
127
|
+
--no-annotate \
|
|
128
|
+
--frozen \
|
|
129
|
+
--output-file "${runtime_requirements_file}"
|
|
130
|
+
|
|
131
|
+
if grep -Eq '(^-e[[:space:]]|^(\.\.?/)|(^|[[:space:]])file:| @ (\.\.?/|file:))' "${runtime_requirements_file}"; then
|
|
132
|
+
echo "::error title=Unexpected local dependency::Runtime requirements must resolve from PyPI."
|
|
133
|
+
cat "${runtime_requirements_file}"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
|
|
118
137
|
- name: Build distributions
|
|
119
138
|
id: build
|
|
120
139
|
run: |
|
|
@@ -174,11 +193,16 @@ jobs:
|
|
|
174
193
|
} >> "${GITHUB_OUTPUT}"
|
|
175
194
|
|
|
176
195
|
- name: Smoke test installed wheel
|
|
196
|
+
env:
|
|
197
|
+
WHEEL_PATH: ${{ steps.build.outputs.wheel_path }}
|
|
177
198
|
run: |
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
python -
|
|
199
|
+
validation_dir="$(mktemp -d)"
|
|
200
|
+
python -m venv "${validation_dir}/venv"
|
|
201
|
+
. "${validation_dir}/venv/bin/activate"
|
|
202
|
+
python -m pip install "${GITHUB_WORKSPACE}/${WHEEL_PATH}"
|
|
203
|
+
(
|
|
204
|
+
cd "${validation_dir}"
|
|
205
|
+
python - <<'PY'
|
|
182
206
|
import os
|
|
183
207
|
|
|
184
208
|
import src_py_lib
|
|
@@ -186,6 +210,7 @@ jobs:
|
|
|
186
210
|
if src_py_lib.__name__ != os.environ["IMPORT_NAME"]:
|
|
187
211
|
raise SystemExit(f"unexpected import name: {src_py_lib.__name__}")
|
|
188
212
|
PY
|
|
213
|
+
)
|
|
189
214
|
|
|
190
215
|
- name: Write release notes
|
|
191
216
|
id: notes
|
|
@@ -238,8 +238,8 @@ jobs:
|
|
|
238
238
|
- name: Install uv
|
|
239
239
|
run: python -m pip install "uv==${UV_VERSION}"
|
|
240
240
|
|
|
241
|
-
- name: Build
|
|
242
|
-
run: uv build --wheel --out-dir dist --no-create-gitignore
|
|
241
|
+
- name: Build distributions
|
|
242
|
+
run: uv build --wheel --sdist --out-dir dist --no-create-gitignore
|
|
243
243
|
|
|
244
244
|
- name: Smoke test installed wheel
|
|
245
245
|
run: |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: src-py-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Reusable libraries for Sourcegraph projects
|
|
5
5
|
Project-URL: Homepage, https://github.com/sourcegraph/src-py-lib
|
|
6
6
|
Project-URL: Issues, https://github.com/sourcegraph/src-py-lib/issues
|
|
@@ -17,8 +17,12 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Typing :: Typed
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
19
|
Requires-Dist: httpx<1,>=0.28.1
|
|
20
|
+
Requires-Dist: opentelemetry-api<2,>=1.38.0
|
|
20
21
|
Requires-Dist: pydantic<3,>=2.13.4
|
|
21
22
|
Requires-Dist: python-dotenv<2,>=1.2.2
|
|
23
|
+
Provides-Extra: otel
|
|
24
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.38.0; extra == 'otel'
|
|
25
|
+
Requires-Dist: opentelemetry-sdk<2,>=1.38.0; extra == 'otel'
|
|
22
26
|
Description-Content-Type: text/markdown
|
|
23
27
|
|
|
24
28
|
# src-py-lib
|
|
@@ -4,6 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[dependency-groups]
|
|
6
6
|
dev = [
|
|
7
|
+
"opentelemetry-exporter-otlp-proto-http>=1.38.0,<2",
|
|
8
|
+
"opentelemetry-sdk>=1.38.0,<2",
|
|
7
9
|
"pyright>=1.1.410",
|
|
8
10
|
"ruff>=0.15.16",
|
|
9
11
|
]
|
|
@@ -30,6 +32,7 @@ classifiers = [
|
|
|
30
32
|
]
|
|
31
33
|
dependencies = [
|
|
32
34
|
"httpx>=0.28.1,<1",
|
|
35
|
+
"opentelemetry-api>=1.38.0,<2",
|
|
33
36
|
"pydantic>=2.13.4,<3",
|
|
34
37
|
"python-dotenv>=1.2.2,<2",
|
|
35
38
|
]
|
|
@@ -41,6 +44,12 @@ keywords = [
|
|
|
41
44
|
Homepage = "https://github.com/sourcegraph/src-py-lib"
|
|
42
45
|
Issues = "https://github.com/sourcegraph/src-py-lib/issues"
|
|
43
46
|
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
otel = [
|
|
49
|
+
"opentelemetry-exporter-otlp-proto-http>=1.38.0,<2",
|
|
50
|
+
"opentelemetry-sdk>=1.38.0,<2",
|
|
51
|
+
]
|
|
52
|
+
|
|
44
53
|
[tool.hatch.build.targets.wheel]
|
|
45
54
|
packages = ["src/src_py_lib"]
|
|
46
55
|
|
|
@@ -73,29 +73,32 @@ from src_py_lib.utils.json_types import (
|
|
|
73
73
|
from src_py_lib.utils.logging import (
|
|
74
74
|
LoggingConfig,
|
|
75
75
|
LoggingSettings,
|
|
76
|
-
TraceContext,
|
|
77
76
|
configure_logging,
|
|
78
77
|
critical,
|
|
79
|
-
current_trace_context,
|
|
80
78
|
debug,
|
|
81
79
|
error,
|
|
82
|
-
event,
|
|
83
80
|
info,
|
|
84
|
-
log,
|
|
85
81
|
log_context,
|
|
82
|
+
log_event,
|
|
86
83
|
logging_context,
|
|
87
84
|
logging_settings_from_config,
|
|
88
|
-
new_trace_context,
|
|
89
85
|
resolve_log_level_name,
|
|
90
|
-
|
|
86
|
+
span,
|
|
91
87
|
stage,
|
|
92
88
|
startup_event,
|
|
93
89
|
submit_with_log_context,
|
|
94
|
-
trace_context,
|
|
95
|
-
trace_context_from_traceparent,
|
|
96
|
-
traceparent_header,
|
|
97
90
|
warning,
|
|
98
91
|
)
|
|
92
|
+
from src_py_lib.utils.telemetry import (
|
|
93
|
+
OpenTelemetryConfig,
|
|
94
|
+
OpenTelemetryRuntime,
|
|
95
|
+
OpenTelemetrySettings,
|
|
96
|
+
OpenTelemetrySetupError,
|
|
97
|
+
configure_open_telemetry,
|
|
98
|
+
current_traceparent_header,
|
|
99
|
+
open_telemetry_settings_from_config,
|
|
100
|
+
traceparent_fields,
|
|
101
|
+
)
|
|
99
102
|
from src_py_lib.utils.tsv import write_tsv
|
|
100
103
|
|
|
101
104
|
|
|
@@ -105,15 +108,33 @@ def logging(
|
|
|
105
108
|
command: str | None = None,
|
|
106
109
|
git_cwd: Path | str | None = None,
|
|
107
110
|
logging_config: LoggingSettings | None = None,
|
|
111
|
+
open_telemetry: OpenTelemetrySettings | None = None,
|
|
108
112
|
run_fields: Mapping[str, Any] | None = None,
|
|
109
113
|
run_summary: Callable[[], Mapping[str, Any]] | None = None,
|
|
110
114
|
) -> AbstractContextManager[Path | None]:
|
|
111
115
|
"""Configure standard CLI logging and emit startup metadata."""
|
|
116
|
+
resolved_logging_config = logging_config
|
|
117
|
+
if open_telemetry is not None:
|
|
118
|
+
resolved_logging_config = logging_config or logging_settings_from_config(config)
|
|
119
|
+
resolved_logging_config = LoggingSettings(
|
|
120
|
+
logger_name=resolved_logging_config.logger_name,
|
|
121
|
+
terminal_level=resolved_logging_config.terminal_level,
|
|
122
|
+
log_file_level=resolved_logging_config.log_file_level,
|
|
123
|
+
log_file=resolved_logging_config.log_file,
|
|
124
|
+
logs_dir=resolved_logging_config.logs_dir,
|
|
125
|
+
run=resolved_logging_config.run,
|
|
126
|
+
retain_log_files=resolved_logging_config.retain_log_files,
|
|
127
|
+
suppress_http_dependency_logs=resolved_logging_config.suppress_http_dependency_logs,
|
|
128
|
+
resource_sample_interval_seconds=(
|
|
129
|
+
resolved_logging_config.resource_sample_interval_seconds
|
|
130
|
+
),
|
|
131
|
+
open_telemetry=open_telemetry,
|
|
132
|
+
)
|
|
112
133
|
return logging_context(
|
|
113
134
|
command or _script_name(),
|
|
114
135
|
config,
|
|
115
136
|
git_cwd=git_cwd,
|
|
116
|
-
logging_config=
|
|
137
|
+
logging_config=resolved_logging_config,
|
|
117
138
|
run_fields=run_fields,
|
|
118
139
|
run_summary=run_summary,
|
|
119
140
|
)
|
|
@@ -139,6 +160,10 @@ __all__ = [
|
|
|
139
160
|
"LinearClientConfig",
|
|
140
161
|
"LoggingConfig",
|
|
141
162
|
"LoggingSettings",
|
|
163
|
+
"OpenTelemetryConfig",
|
|
164
|
+
"OpenTelemetryRuntime",
|
|
165
|
+
"OpenTelemetrySettings",
|
|
166
|
+
"OpenTelemetrySetupError",
|
|
142
167
|
"PullRequest",
|
|
143
168
|
"SlackClient",
|
|
144
169
|
"SlackClientConfig",
|
|
@@ -149,15 +174,15 @@ __all__ = [
|
|
|
149
174
|
"SourcegraphJaegerTraceError",
|
|
150
175
|
"SourcegraphJaegerTraceSummary",
|
|
151
176
|
"SourcegraphTrace",
|
|
152
|
-
"TraceContext",
|
|
153
177
|
"aliased_batched_query",
|
|
154
178
|
"config_field",
|
|
155
179
|
"config_field_names",
|
|
156
180
|
"config_help_formatter",
|
|
157
181
|
"config_snapshot",
|
|
182
|
+
"configure_open_telemetry",
|
|
158
183
|
"configure_logging",
|
|
159
184
|
"critical",
|
|
160
|
-
"
|
|
185
|
+
"current_traceparent_header",
|
|
161
186
|
"debug",
|
|
162
187
|
"decode_external_service_id",
|
|
163
188
|
"decode_repository_id",
|
|
@@ -165,7 +190,7 @@ __all__ = [
|
|
|
165
190
|
"encode_repository_id",
|
|
166
191
|
"encode_sourcegraph_node_id",
|
|
167
192
|
"error",
|
|
168
|
-
"
|
|
193
|
+
"span",
|
|
169
194
|
"gh_cli_token",
|
|
170
195
|
"gcloud_adc_access_token",
|
|
171
196
|
"info",
|
|
@@ -182,25 +207,22 @@ __all__ = [
|
|
|
182
207
|
"logging",
|
|
183
208
|
"logging_context",
|
|
184
209
|
"logging_settings_from_config",
|
|
185
|
-
"
|
|
210
|
+
"log_event",
|
|
186
211
|
"log_context",
|
|
187
|
-
"new_trace_context",
|
|
188
212
|
"normalize_sourcegraph_endpoint",
|
|
213
|
+
"open_telemetry_settings_from_config",
|
|
189
214
|
"parse_args",
|
|
190
215
|
"pr_ref_from_url",
|
|
191
216
|
"quota_project_from_adc",
|
|
192
217
|
"resolve_log_level_name",
|
|
193
218
|
"save_json_cache",
|
|
194
|
-
"sampled_traceparent",
|
|
195
219
|
"slack_client_from_config",
|
|
196
220
|
"sourcegraph_client_from_config",
|
|
197
221
|
"stage",
|
|
198
222
|
"startup_event",
|
|
199
223
|
"stream_connection_nodes",
|
|
200
224
|
"submit_with_log_context",
|
|
201
|
-
"
|
|
202
|
-
"trace_context_from_traceparent",
|
|
203
|
-
"traceparent_header",
|
|
225
|
+
"traceparent_fields",
|
|
204
226
|
"warning",
|
|
205
227
|
"write_tsv",
|
|
206
228
|
]
|
|
@@ -11,7 +11,7 @@ from typing import cast
|
|
|
11
11
|
|
|
12
12
|
from src_py_lib.utils.http import HTTPClient, HTTPClientError, HTTPResponse, log_safe_url
|
|
13
13
|
from src_py_lib.utils.json_types import JSONDict, JSONValue, json_dict, json_list, json_str
|
|
14
|
-
from src_py_lib.utils.logging import
|
|
14
|
+
from src_py_lib.utils.logging import span
|
|
15
15
|
|
|
16
16
|
_OPERATION_NAME_RE = re.compile(r"\b(?:query|mutation|subscription)\s+(\w+)")
|
|
17
17
|
HeaderProvider = Mapping[str, str] | Callable[[], Mapping[str, str]]
|
|
@@ -241,7 +241,7 @@ class GraphQLClient:
|
|
|
241
241
|
after_variable: str = "after",
|
|
242
242
|
) -> JSONDict:
|
|
243
243
|
body = {"query": query, "variables": variables or {}}
|
|
244
|
-
with
|
|
244
|
+
with span(
|
|
245
245
|
"graphql_query",
|
|
246
246
|
level="debug",
|
|
247
247
|
graphql_client=self.label,
|
|
@@ -10,19 +10,18 @@ import time
|
|
|
10
10
|
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
|
11
11
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
12
12
|
from dataclasses import dataclass, field
|
|
13
|
-
from typing import Final
|
|
13
|
+
from typing import Final
|
|
14
14
|
from urllib.parse import urlsplit
|
|
15
15
|
|
|
16
16
|
from src_py_lib.clients.graphql import GraphQLClient, stream_connection_nodes
|
|
17
17
|
from src_py_lib.utils.config import Config, config_field
|
|
18
18
|
from src_py_lib.utils.http import HTTPClient, HTTPClientError, HTTPResponse
|
|
19
19
|
from src_py_lib.utils.json_types import JSONDict, JSONValue, json_dict, json_list
|
|
20
|
-
from src_py_lib.utils.logging import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
traceparent_header,
|
|
20
|
+
from src_py_lib.utils.logging import submit_with_log_context
|
|
21
|
+
from src_py_lib.utils.telemetry import (
|
|
22
|
+
current_traceparent_header,
|
|
23
|
+
set_current_span_attributes,
|
|
24
|
+
traceparent_fields,
|
|
26
25
|
)
|
|
27
26
|
|
|
28
27
|
SOURCEGRAPH_EXTERNAL_SERVICE_NODE_TYPE: Final[str] = "ExternalService"
|
|
@@ -187,16 +186,16 @@ class SourcegraphClient:
|
|
|
187
186
|
Plain HTTP endpoints are rejected unless `allow_insecure_http=True` is set
|
|
188
187
|
for local development.
|
|
189
188
|
|
|
190
|
-
Set `
|
|
191
|
-
request. Traced requests are available through `drain_traces()` and
|
|
192
|
-
fetched from the instance's Jaeger/debug endpoint with
|
|
189
|
+
Set `fetch_sg_traces=True` to ask Sourcegraph to retain traces for each
|
|
190
|
+
GraphQL request. Traced requests are available through `drain_traces()` and
|
|
191
|
+
can be fetched from the instance's Jaeger/debug endpoint with
|
|
193
192
|
`stream_jaeger_trace_summaries()`.
|
|
194
193
|
"""
|
|
195
194
|
|
|
196
195
|
endpoint: str
|
|
197
196
|
token: str
|
|
198
197
|
http: HTTPClient = field(default_factory=HTTPClient)
|
|
199
|
-
|
|
198
|
+
fetch_sg_traces: bool = False
|
|
200
199
|
allow_insecure_http: bool = False
|
|
201
200
|
_traces: queue.Queue[SourcegraphTrace] = field(
|
|
202
201
|
default_factory=lambda: queue.Queue[SourcegraphTrace](), init=False, repr=False
|
|
@@ -355,7 +354,7 @@ class SourcegraphClient:
|
|
|
355
354
|
headers=self._graphql_headers,
|
|
356
355
|
label="Sourcegraph",
|
|
357
356
|
http=self.http,
|
|
358
|
-
response_hook=self._record_trace_response if self.
|
|
357
|
+
response_hook=self._record_trace_response if self.fetch_sg_traces else None,
|
|
359
358
|
)
|
|
360
359
|
|
|
361
360
|
def _authorization_headers(self) -> dict[str, str]:
|
|
@@ -363,11 +362,11 @@ class SourcegraphClient:
|
|
|
363
362
|
|
|
364
363
|
def _graphql_headers(self) -> dict[str, str]:
|
|
365
364
|
headers = self._authorization_headers()
|
|
366
|
-
if self.
|
|
365
|
+
if self.fetch_sg_traces:
|
|
367
366
|
headers[REQUEST_TRACE_HEADER] = "true"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
traceparent = current_traceparent_header()
|
|
368
|
+
if traceparent is not None:
|
|
369
|
+
headers[TRACEPARENT_HEADER] = traceparent
|
|
371
370
|
return headers
|
|
372
371
|
|
|
373
372
|
def _record_trace_response(
|
|
@@ -375,6 +374,13 @@ class SourcegraphClient:
|
|
|
375
374
|
) -> None:
|
|
376
375
|
trace = sourcegraph_trace_from_headers(response.headers, request_headers)
|
|
377
376
|
if trace is not None:
|
|
377
|
+
set_current_span_attributes(
|
|
378
|
+
{
|
|
379
|
+
"sourcegraph.trace_id": trace.trace_id,
|
|
380
|
+
"sourcegraph.trace_url": trace.trace_url,
|
|
381
|
+
"sourcegraph.span_id": trace.span_id,
|
|
382
|
+
}
|
|
383
|
+
)
|
|
378
384
|
self._traces.put(trace)
|
|
379
385
|
|
|
380
386
|
|
|
@@ -382,22 +388,17 @@ def sourcegraph_client_from_config(
|
|
|
382
388
|
config: SourcegraphClientConfig,
|
|
383
389
|
*,
|
|
384
390
|
http: HTTPClient | None = None,
|
|
385
|
-
|
|
391
|
+
fetch_sg_traces: bool = False,
|
|
386
392
|
) -> SourcegraphClient:
|
|
387
393
|
"""Return a Sourcegraph API client from shared Sourcegraph Config fields."""
|
|
388
394
|
return SourcegraphClient(
|
|
389
395
|
endpoint=config.src_endpoint,
|
|
390
396
|
token=config.src_access_token,
|
|
391
397
|
http=http or HTTPClient(),
|
|
392
|
-
|
|
398
|
+
fetch_sg_traces=fetch_sg_traces,
|
|
393
399
|
)
|
|
394
400
|
|
|
395
401
|
|
|
396
|
-
def sampled_traceparent() -> str:
|
|
397
|
-
"""Compatibility wrapper for sampled W3C traceparent generation."""
|
|
398
|
-
return traceparent_header(sampled=True)
|
|
399
|
-
|
|
400
|
-
|
|
401
402
|
def sourcegraph_trace_from_headers(
|
|
402
403
|
response_headers: Mapping[str, str], request_headers: Mapping[str, str]
|
|
403
404
|
) -> SourcegraphTrace | None:
|
|
@@ -407,13 +408,13 @@ def sourcegraph_trace_from_headers(
|
|
|
407
408
|
return None
|
|
408
409
|
span_id = header_value(response_headers, TRACE_SPAN_RESPONSE_HEADER)
|
|
409
410
|
trace_url = header_value(response_headers, TRACE_URL_RESPONSE_HEADER)
|
|
410
|
-
parent =
|
|
411
|
+
parent = traceparent_fields(header_value(request_headers, TRACEPARENT_HEADER))
|
|
411
412
|
return SourcegraphTrace(
|
|
412
413
|
trace_id=trace_id.lower(),
|
|
413
414
|
span_id=span_id.lower() if span_id and is_hex_identifier(span_id, 16) else span_id,
|
|
414
415
|
trace_url=trace_url,
|
|
415
|
-
parent_trace_id=parent.trace_id
|
|
416
|
-
parent_span_id=parent.span_id
|
|
416
|
+
parent_trace_id=parent.get("trace_id"),
|
|
417
|
+
parent_span_id=parent.get("span_id"),
|
|
417
418
|
)
|
|
418
419
|
|
|
419
420
|
|
|
@@ -472,7 +473,7 @@ def summarize_jaeger_trace(
|
|
|
472
473
|
}
|
|
473
474
|
)
|
|
474
475
|
|
|
475
|
-
hot_operations = [
|
|
476
|
+
hot_operations: list[JSONDict] = [
|
|
476
477
|
{
|
|
477
478
|
"operation": operation,
|
|
478
479
|
"count": len(durations),
|
|
@@ -481,12 +482,12 @@ def summarize_jaeger_trace(
|
|
|
481
482
|
}
|
|
482
483
|
for operation, durations in durations_by_operation.items()
|
|
483
484
|
]
|
|
484
|
-
hot_operations.sort(key=
|
|
485
|
+
hot_operations.sort(key=jaeger_summary_operation_sum_ms, reverse=True)
|
|
485
486
|
return SourcegraphJaegerTraceSummary(
|
|
486
487
|
trace=trace_metadata,
|
|
487
488
|
jaeger_found=True,
|
|
488
489
|
span_count=len(spans),
|
|
489
|
-
hot_operations=tuple(
|
|
490
|
+
hot_operations=tuple(hot_operations[:10]),
|
|
490
491
|
graphql_operations=tuple(
|
|
491
492
|
{"operation": operation, "count": count}
|
|
492
493
|
for operation, count in graphql_operations.most_common(10)
|
|
@@ -495,6 +496,11 @@ def summarize_jaeger_trace(
|
|
|
495
496
|
)
|
|
496
497
|
|
|
497
498
|
|
|
499
|
+
def jaeger_summary_operation_sum_ms(operation: JSONDict) -> float:
|
|
500
|
+
"""Return the total duration for sorting compact Jaeger operation summaries."""
|
|
501
|
+
return float_value(operation.get("sum_ms"))
|
|
502
|
+
|
|
503
|
+
|
|
498
504
|
def jaeger_span_tags(span: JSONDict) -> dict[str, object]:
|
|
499
505
|
"""Return Jaeger span tags keyed by tag name."""
|
|
500
506
|
tags: dict[str, object] = {}
|
|
@@ -14,7 +14,13 @@ from typing import Final, cast
|
|
|
14
14
|
import httpx
|
|
15
15
|
|
|
16
16
|
from src_py_lib.utils.json_types import JSONDict, json_dict
|
|
17
|
-
from src_py_lib.utils.logging import
|
|
17
|
+
from src_py_lib.utils.logging import record_http_attempt, record_http_retry, span
|
|
18
|
+
from src_py_lib.utils.telemetry import (
|
|
19
|
+
inject_current_trace_context,
|
|
20
|
+
mark_current_span_error,
|
|
21
|
+
record_http_client_metrics,
|
|
22
|
+
record_http_client_retry,
|
|
23
|
+
)
|
|
18
24
|
|
|
19
25
|
DEFAULT_TIMEOUT_SECONDS: Final[float] = 30.0
|
|
20
26
|
DEFAULT_MAX_CONNECTIONS: Final[int] = 20
|
|
@@ -158,8 +164,9 @@ class HTTPClient:
|
|
|
158
164
|
body = json.dumps(json_body).encode("utf-8")
|
|
159
165
|
request_headers.setdefault("Content-Type", "application/json")
|
|
160
166
|
for attempt in range(1, self.max_attempts + 1):
|
|
167
|
+
attempt_started = time.perf_counter()
|
|
161
168
|
try:
|
|
162
|
-
with
|
|
169
|
+
with span(
|
|
163
170
|
"http_request",
|
|
164
171
|
level="debug",
|
|
165
172
|
method=method,
|
|
@@ -168,6 +175,7 @@ class HTTPClient:
|
|
|
168
175
|
request_headers=_headers_for_log(request_headers),
|
|
169
176
|
request_bytes=len(body or b""),
|
|
170
177
|
) as fields:
|
|
178
|
+
inject_current_trace_context(request_headers)
|
|
171
179
|
response = self._client.request(
|
|
172
180
|
method,
|
|
173
181
|
request_url,
|
|
@@ -182,12 +190,22 @@ class HTTPClient:
|
|
|
182
190
|
http_version = _response_http_version(response)
|
|
183
191
|
if http_version is not None:
|
|
184
192
|
fields["http_version"] = http_version
|
|
193
|
+
duration_seconds = time.perf_counter() - attempt_started
|
|
185
194
|
record_http_attempt(
|
|
186
195
|
request_bytes=len(body or b""),
|
|
187
196
|
response_bytes=len(payload),
|
|
188
197
|
status_code=response.status_code,
|
|
189
198
|
)
|
|
199
|
+
record_http_client_metrics(
|
|
200
|
+
method=method,
|
|
201
|
+
url=request_url,
|
|
202
|
+
duration_seconds=duration_seconds,
|
|
203
|
+
request_bytes=len(body or b""),
|
|
204
|
+
response_bytes=len(payload),
|
|
205
|
+
status_code=response.status_code,
|
|
206
|
+
)
|
|
190
207
|
if response.status_code >= 400:
|
|
208
|
+
mark_current_span_error(f"HTTP {response.status_code}")
|
|
191
209
|
body_text = _body_preview(payload)
|
|
192
210
|
if not self._should_retry(response.status_code, attempt):
|
|
193
211
|
raise HTTPClientError(
|
|
@@ -198,6 +216,11 @@ class HTTPClient:
|
|
|
198
216
|
headers=dict(response.headers),
|
|
199
217
|
)
|
|
200
218
|
record_http_retry()
|
|
219
|
+
record_http_client_retry(
|
|
220
|
+
method=method,
|
|
221
|
+
url=request_url,
|
|
222
|
+
status_code=response.status_code,
|
|
223
|
+
)
|
|
201
224
|
self._sleep_before_retry(attempt, response.headers.get("Retry-After"))
|
|
202
225
|
else:
|
|
203
226
|
return HTTPResponse(
|
|
@@ -210,7 +233,15 @@ class HTTPClient:
|
|
|
210
233
|
except HTTPClientError:
|
|
211
234
|
raise
|
|
212
235
|
except httpx.TransportError as exception:
|
|
236
|
+
duration_seconds = time.perf_counter() - attempt_started
|
|
213
237
|
record_http_attempt(request_bytes=len(body or b""), transport_error=True)
|
|
238
|
+
record_http_client_metrics(
|
|
239
|
+
method=method,
|
|
240
|
+
url=request_url,
|
|
241
|
+
duration_seconds=duration_seconds,
|
|
242
|
+
request_bytes=len(body or b""),
|
|
243
|
+
transport_error=True,
|
|
244
|
+
)
|
|
214
245
|
if not self._should_retry(None, attempt):
|
|
215
246
|
failure = (
|
|
216
247
|
"timed out" if isinstance(exception, httpx.TimeoutException) else "failed"
|
|
@@ -220,6 +251,7 @@ class HTTPClient:
|
|
|
220
251
|
f"{_exception_message(exception)}"
|
|
221
252
|
) from exception
|
|
222
253
|
record_http_retry()
|
|
254
|
+
record_http_client_retry(method=method, url=request_url)
|
|
223
255
|
self._sleep_before_retry(attempt, None)
|
|
224
256
|
raise AssertionError("HTTP retry loop exited without returning or raising")
|
|
225
257
|
|