Skip to content

Commit 91b5017

Browse files
authored
Request activity (Azure#41013)
* Update _base.py * Update _cosmos_http_logging_policy.py * Update CHANGELOG.md * Update CHANGELOG.md * update activity id with tests Added tests to verify Activity id is generated client side. Proof of client generated activity id can be seen from seeing it in the request headers. * update test
1 parent 8d2a46a commit 91b5017

File tree

5 files changed

+126
-9
lines changed

5 files changed

+126
-9
lines changed

sdk/cosmos/azure-cosmos/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Fixed issue where Query Change Feed did not return items if the container uses legacy Hash V1 Partition Keys. This also fixes issues with not being able to change feed query for Specific Partition Key Values for HPK. See [PR 41270](https://github.com/Azure/azure-sdk-for-python/pull/41270/)
1111

1212
#### Other Changes
13+
* Added Client Generated Activity IDs to all Requests. Cosmos Diagnostics Logs will more clearly show the Activity ID for each request and response. [PR 41013](https://github.com/Azure/azure-sdk-for-python/pull/41013)
1314

1415
### 4.12.0b1 (2025-05-19)
1516

sdk/cosmos/azure-cosmos/azure/cosmos/_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ def GetHeaders( # pylint: disable=too-many-statements,too-many-branches
142142
headers = dict(default_headers)
143143
options = options or {}
144144

145+
# Generate a new activity ID for each request client side.
146+
headers[http_constants.HttpHeaders.ActivityId] = GenerateGuidId()
145147
if cosmos_client_connection.UseMultipleWriteLocations:
146148
headers[http_constants.HttpHeaders.AllowTentativeWrites] = "true"
147149

sdk/cosmos/azure-cosmos/azure/cosmos/_cosmos_http_logging_policy.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ def on_request(
127127
filter_applied = bool(logger.filters) or any(bool(h.filters) for h in logger.handlers)
128128
if filter_applied and 'logger_attributes' not in request.context:
129129
return
130-
operation_type = http_request.headers.get('x-ms-thinclient-proxy-operation-type')
130+
operation_type = http_request.headers.get('x-ms-thinclient-proxy-operation-type', "")
131131
try:
132132
url = request.http_request.url
133133
except AttributeError:
134134
url = None
135135
database_name = None
136136
collection_name = None
137-
resource_type = http_request.headers.get('x-ms-thinclient-proxy-resource-type')
137+
resource_type = http_request.headers.get('x-ms-thinclient-proxy-resource-type', "")
138138
if url:
139139
url_parts = url.split('/')
140140
if 'dbs' in url_parts:
@@ -160,9 +160,11 @@ def on_request(
160160

161161
if filter_applied and 'logger_attributes' in request.context:
162162
cosmos_logger_attributes = request.context['logger_attributes']
163+
cosmos_logger_attributes['activity_id'] = http_request.headers.get(HttpHeaders.ActivityId, "")
163164
cosmos_logger_attributes['is_request'] = True
164165
else:
165166
cosmos_logger_attributes = {
167+
'activity_id': http_request.headers.get(HttpHeaders.ActivityId, ""),
166168
'duration': None,
167169
'status_code': None,
168170
'sub_status_code': None,
@@ -181,6 +183,8 @@ def on_request(
181183
logger.info(db_settings, extra=cosmos_logger_attributes)
182184
logger.info("Request URL: %r", redacted_url, extra=cosmos_logger_attributes)
183185
logger.info("Request method: %r", http_request.method, extra=cosmos_logger_attributes)
186+
logger.info("Request Activity ID: %r", http_request.headers.get(HttpHeaders.ActivityId),
187+
extra=cosmos_logger_attributes)
184188
logger.info("Request headers:", extra=cosmos_logger_attributes)
185189
for header, value in http_request.headers.items():
186190
value = self._redact_header(header, value)
@@ -204,6 +208,7 @@ def on_request(
204208
log_string += db_settings
205209
log_string += "\nRequest URL: '{}'".format(redacted_url)
206210
log_string += "\nRequest method: '{}'".format(http_request.method)
211+
log_string += "\nRequest Activity ID: {}".format(http_request.headers.get(HttpHeaders.ActivityId))
207212
log_string += "\nRequest headers:"
208213
for header, value in http_request.headers.items():
209214
value = self._redact_header(header, value)
@@ -251,12 +256,13 @@ def on_response( # pylint: disable=too-many-statements, too-many-branches, too-
251256
duration = (time.time() - context["start_time"]) * 1000 \
252257
if "start_time" in context else None # type: ignore[union-attr, arg-type]
253258

254-
log_data = {"duration": duration,
259+
log_data = {"activity_id": http_response.headers.get(HttpHeaders.ActivityId, ""),
260+
"duration": duration,
255261
"status_code": http_response.status_code, "sub_status_code": sub_status_code,
256262
"verb": request.http_request.method,
257-
"operation_type": headers.get('x-ms-thinclient-proxy-operation-type'),
263+
"operation_type": headers.get('x-ms-thinclient-proxy-operation-type', ""),
258264
"url": str(url_obj), "database_name": "", "collection_name": "",
259-
"resource_type": headers.get('x-ms-thinclient-proxy-resource-type'), "is_request": False} # type: ignore[assignment] # pylint: disable=line-too-long
265+
"resource_type": headers.get('x-ms-thinclient-proxy-resource-type', ""), "is_request": False} # type: ignore[assignment] # pylint: disable=line-too-long
260266

261267
if log_data["url"]:
262268
url_parts: List[str] = log_data["url"].split('/') # type: ignore[union-attr]

sdk/cosmos/azure-cosmos/tests/test_cosmos_http_logging_policy.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def test_default_http_logging_policy(self):
122122
messages_response = self.mock_handler_default.messages[1].message.split("\n")
123123
assert messages_request[1] == "Request method: 'GET'"
124124
assert 'Request headers:' in messages_request[2]
125-
assert messages_request[14] == 'No body was attached to the request'
125+
assert messages_request[-1] == 'No body was attached to the request'
126126
assert messages_response[0] == 'Response status: 200'
127127
assert 'Response headers:' in messages_response[1]
128128

@@ -286,5 +286,59 @@ def test_client_settings(self):
286286
locations = get_locations_list(message)
287287
assert all_locations == locations
288288

289+
def test_activity_id_logging_policy(self):
290+
# Create a mock handler and logger for the new client
291+
self.mock_handler_activity_id = MockHandler()
292+
self.logger_activity_id = create_logger("test_logger_activity_id", self.mock_handler_activity_id)
293+
294+
# Create a new client with the logger and enable diagnostics logging
295+
self.client_activity_id = cosmos_client.CosmosClient(
296+
self.host,
297+
self.masterKey,
298+
consistency_level="Session",
299+
connection_policy=self.connectionPolicy,
300+
logger=self.logger_activity_id,
301+
enable_diagnostics_logging=True
302+
)
303+
304+
# Generate a custom activity ID
305+
custom_activity_id = str(uuid.uuid4())
306+
307+
# Create a database and container for the test
308+
database_id = "database_test_activity_id_" + str(uuid.uuid4())
309+
container_id = "container_test_activity_id_" + str(uuid.uuid4())
310+
database = self.client_activity_id.create_database(id=database_id)
311+
container = database.create_container(id=container_id, partition_key=PartitionKey(path="/pk"))
312+
# Reset the mock handler to clear previous messages
313+
self.mock_handler_activity_id.reset()
314+
315+
# Upsert an item and verify the request and response activity IDs match
316+
item_id = str(uuid.uuid4())
317+
item_body = {"id": item_id, "pk": item_id}
318+
container.upsert_item(body=item_body)
319+
320+
# Verify that the request activity ID matches the response activity ID
321+
# Having the Request Activity confirms we generated one from SDK
322+
# Having it match the response means it was passed successfully
323+
log_record_request = self.mock_handler_activity_id.messages[0]
324+
log_record_response = self.mock_handler_activity_id.messages[1]
325+
assert log_record_request.activity_id == log_record_response.activity_id
326+
327+
# Upsert another item with the custom activity ID in the initial headers
328+
headers = {"x-ms-activity-id": custom_activity_id}
329+
item_id_2 = str(uuid.uuid4())
330+
item_body_2 = {"id": item_id_2, "pk": item_id_2}
331+
container.upsert_item(body=item_body_2, initial_headers=headers)
332+
333+
# Verify that the custom activity ID does not match the request activity ID from the log record
334+
# Users should not be able to pass in their own activity ID.
335+
log_record_request_2 = self.mock_handler_activity_id.messages[2]
336+
assert log_record_request_2.activity_id != custom_activity_id
337+
338+
# Clean up by deleting the database
339+
self.client_activity_id.delete_database(database_id)
340+
self.mock_handler_activity_id.reset()
341+
342+
289343
if __name__ == "__main__":
290344
unittest.main()

sdk/cosmos/azure-cosmos/tests/test_cosmos_http_logging_policy_async.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
class TestCosmosHttpLoggerAsync(unittest.IsolatedAsyncioTestCase):
2828
mock_handler_diagnostic = None
2929
mock_handler_default = None
30-
logger_diagnostic = None
31-
logger_default = None
3230
config = test_config.TestConfig
3331
host = config.host
3432
masterKey = config.masterKey
@@ -47,6 +45,7 @@ async def asyncSetUp(self):
4745
self.mock_handler_default = MockHandler()
4846
self.mock_handler_diagnostic = MockHandler()
4947
self.mock_handler_filtered_diagnostic = MockHandler()
48+
self.mock_handler_activity_id = MockHandler()
5049
# Add filter to the filtered diagnostics handler
5150

5251
self.mock_handler_filtered_diagnostic.addFilter(FilterStatusCode())
@@ -57,6 +56,7 @@ async def asyncSetUp(self):
5756
self.client_default = None
5857
self.client_diagnostic = None
5958
self.client_filtered_diagnostic = None
59+
self.client_activity_id = None
6060

6161
async def asyncTearDown(self):
6262
if self.client_default:
@@ -65,6 +65,8 @@ async def asyncTearDown(self):
6565
await self.client_diagnostic.close()
6666
if self.client_filtered_diagnostic:
6767
await self.client_filtered_diagnostic.close()
68+
if self.client_activity_id:
69+
await self.client_activity_id.close()
6870

6971
async def test_default_http_logging_policy_async(self):
7072
self.logger.addHandler(self.mock_handler_default)
@@ -81,7 +83,7 @@ async def test_default_http_logging_policy_async(self):
8183
messages_response = self.mock_handler_default.messages[1].message.split("\n")
8284
assert messages_request[1] == "Request method: 'GET'"
8385
assert 'Request headers:' in messages_request[2]
84-
assert messages_request[15] == 'No body was attached to the request'
86+
assert messages_request[-1] == 'No body was attached to the request'
8587
assert messages_response[0] == 'Response status: 200'
8688
assert 'Response headers:' in messages_response[1]
8789

@@ -257,6 +259,58 @@ async def test_client_settings_async(self):
257259
assert all_locations == locations
258260
await initialized_objects["client"].close()
259261

262+
async def test_activity_id_logging_policy_async(self):
263+
# Create a mock handler and logger for the new client
264+
self.logger.addHandler(self.mock_handler_activity_id)
265+
266+
# Create a new client with the logger and enable diagnostics logging
267+
self.client_activity_id = cosmos_client.CosmosClient(
268+
self.host,
269+
self.masterKey,
270+
consistency_level="Session",
271+
logger=self.logger,
272+
enable_diagnostics_logging=True
273+
)
274+
# Generate a custom activity ID
275+
custom_activity_id = str(uuid.uuid4())
276+
277+
# Create a database and container for the test
278+
database_id = "database_test_activity_id_" + str(uuid.uuid4())
279+
container_id = "container_test_activity_id_" + str(uuid.uuid4())
280+
try:
281+
282+
database = await self.client_activity_id.create_database(id=database_id)
283+
container = await database.create_container(id=container_id, partition_key=PartitionKey(path="/pk"))
284+
285+
# Reset the mock handler to clear previous messages
286+
self.mock_handler_activity_id.reset()
287+
288+
# Upsert an item and verify the request and response activity IDs match
289+
item_id = str(uuid.uuid4())
290+
item_body = {"id": item_id, "pk": item_id}
291+
await container.upsert_item(body=item_body)
292+
293+
# Verify that the request activity ID matches the response activity ID
294+
log_record_request = self.mock_handler_activity_id.messages[0]
295+
log_record_response = self.mock_handler_activity_id.messages[1]
296+
assert log_record_request.activity_id == log_record_response.activity_id
297+
298+
# Upsert another item with the custom activity ID in the initial headers
299+
headers = {"x-ms-activity-id": custom_activity_id}
300+
item_id_2 = str(uuid.uuid4())
301+
item_body_2 = {"id": item_id_2, "pk": item_id_2}
302+
await container.upsert_item(body=item_body_2, initial_headers=headers)
303+
304+
# Verify that the custom activity ID does not match the request activity ID from the log record
305+
log_record_request_2 = self.mock_handler_activity_id.messages[2]
306+
assert log_record_request_2.activity_id != custom_activity_id
307+
308+
finally:
309+
# Clean up by deleting the database
310+
await self.client_activity_id.delete_database(database_id)
311+
self.mock_handler_activity_id.reset()
312+
self.logger.removeHandler(self.mock_handler_activity_id)
313+
260314

261315
if __name__ == "__main__":
262316
unittest.main()

0 commit comments

Comments
 (0)