Skip to content

Commit 825b9f7

Browse files
authored
Merge pull request #40 from nasa/harmony-1719
HARMONY-1719: Add request-id to download requests to support Cloud Metrics
2 parents aafe2a0 + 6cd141c commit 825b9f7

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

harmony/adapter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from pystac import Catalog, Item, Asset, read_file
2222

2323
from harmony.exceptions import CanceledException
24+
from harmony.http import request_context
2425
from harmony.logging import build_logger
2526
from harmony.message import Temporal
2627
from harmony.util import touch_health_check_file
@@ -70,6 +71,10 @@ def __init__(self, message, catalog=None, config=None):
7071
warn('Invoking adapter.BaseHarmonyAdapter without a STAC catalog is deprecated',
7172
DeprecationWarning, stacklevel=2)
7273

74+
# set the request ID in the global context so we can use it in other places
75+
request_id = message.requestId if hasattr(message, 'requestId') else None
76+
request_context['request_id'] = request_id
77+
7378
self.message = message
7479
self.catalog = catalog
7580
self.config = config

harmony/http.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from functools import lru_cache
1414
import json
1515
from time import sleep
16-
from urllib.parse import urlparse
16+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
1717
import datetime
1818
import sys
1919
import os
@@ -35,6 +35,10 @@
3535

3636
MAX_RETRY_DELAY_SECS = 90
3737

38+
# `request_context` is used to provide information about the request to functions like `download`
39+
# without adding extra function arguments
40+
request_context = {}
41+
3842

3943
def get_retry_delay(retry_num: int, max_delay: int = MAX_RETRY_DELAY_SECS) -> int:
4044
"""The number of seconds to sleep before retrying. Exponential backoff starting
@@ -132,6 +136,37 @@ def _earthdata_session():
132136
return EarthdataSession()
133137

134138

139+
def _add_api_request_uuid(url):
140+
request_id = request_context.get('request_id')
141+
142+
if request_id is None:
143+
return url
144+
145+
# Parse the URL into components
146+
parsed_url = urlparse(url)
147+
148+
# only add the request_id if this is an http/https url
149+
if parsed_url.scheme != 'http' and parsed_url.scheme != 'https':
150+
return url
151+
152+
# Extract the current query parameters from the URL
153+
query_params = parse_qs(parsed_url.query)
154+
155+
# Add or update the 'request_id' parameter
156+
query_params['A-api-request-uuid'] = request_id
157+
158+
# Convert the query parameters back to a string
159+
query_string = urlencode(query_params, doseq=True)
160+
161+
# Rebuild the URL with the new query string
162+
new_url = urlunparse(
163+
(parsed_url.scheme, parsed_url.netloc, parsed_url.path,
164+
parsed_url.params, query_string, parsed_url.fragment)
165+
)
166+
167+
return new_url
168+
169+
135170
def _download(
136171
config, url: str,
137172
access_token: str,
@@ -359,6 +394,8 @@ def download(config, url: str, access_token: str, data, destination_file,
359394

360395
response = None
361396
logger = build_logger(config)
397+
# Add the request ID to the download url so it can be used by Cloud Metrics
398+
url = _add_api_request_uuid(url)
362399
start_time = datetime.datetime.now()
363400
logger.info(f'timing.download.start {url}')
364401

tests/test_util.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import pathlib
2+
from requests import Session
23
import unittest
34
from unittest.mock import patch, MagicMock, mock_open, ANY
45
from urllib.error import HTTPError
56

67
from harmony import aws
78
from harmony import util
9+
from harmony.http import request_context
810
from harmony.message import Variable
911
from tests.test_cli import MockAdapter, cli_test
1012
from tests.util import mock_receive, config_fixture
@@ -19,7 +21,7 @@ def setUp(self):
1921
@patch('harmony.aws.Config')
2022
def test_s3_download_sets_minimal_user_agent_on_boto_client(self, boto_cfg, client, get_version):
2123
fake_lib_version = '0.1.0'
22-
get_version.return_value = fake_lib_version
24+
get_version.return_value = fake_lib_version
2325
cfg = config_fixture()
2426
boto_cfg_instance = MagicMock()
2527
boto_cfg.return_value = boto_cfg_instance
@@ -33,7 +35,7 @@ def test_s3_download_sets_minimal_user_agent_on_boto_client(self, boto_cfg, clie
3335
@patch('harmony.aws.Config')
3436
def test_s3_download_sets_harmony_user_agent_on_boto_client(self, boto_cfg, client, get_version):
3537
fake_lib_version = '0.1.0'
36-
get_version.return_value = fake_lib_version
38+
get_version.return_value = fake_lib_version
3739
harmony_user_agt = 'harmony/3.3.3 (harmony-test)'
3840
cfg = config_fixture(user_agent=harmony_user_agt)
3941
boto_cfg_instance = MagicMock()
@@ -49,7 +51,7 @@ def test_s3_download_sets_harmony_user_agent_on_boto_client(self, boto_cfg, clie
4951
def test_s3_download_sets_app_name_on_boto_client(self, boto_cfg, client, get_version):
5052
app_name = 'gdal-subsetter'
5153
fake_lib_version = '0.1.0'
52-
get_version.return_value = fake_lib_version
54+
get_version.return_value = fake_lib_version
5355
cfg = config_fixture(app_name=app_name)
5456
boto_cfg_instance = MagicMock()
5557
boto_cfg.return_value = boto_cfg_instance
@@ -58,6 +60,73 @@ def test_s3_download_sets_app_name_on_boto_client(self, boto_cfg, client, get_ve
5860
boto_cfg.assert_called_with(user_agent_extra=f'harmony (unknown version) harmony-service-lib/{fake_lib_version} ({app_name})')
5961
client.assert_called_with(service_name='s3', config=boto_cfg_instance, region_name=ANY)
6062

63+
@patch('harmony.util.get_version')
64+
@patch('harmony.aws.download')
65+
@patch('harmony.aws.Config')
66+
def test_s3_download_does_not_set_api_request_uuid(self, boto_cfg, aws_download, get_version):
67+
request_context['request_id'] = 'abc123'
68+
app_name = 'gdal-subsetter'
69+
fake_lib_version = '0.1.0'
70+
get_version.return_value = fake_lib_version
71+
cfg = config_fixture(app_name=app_name)
72+
boto_cfg_instance = MagicMock()
73+
boto_cfg.return_value = boto_cfg_instance
74+
with patch('builtins.open', mock_open()):
75+
util.download('s3://example/file.txt', 'tmp', access_token='', cfg=cfg)
76+
aws_download.assert_called_with(ANY, 's3://example/file.txt', ANY, ANY )
77+
78+
@patch('harmony.util.get_version')
79+
@patch.object(Session, 'get')
80+
def test_http_download_sets_api_request_uuid(self, get, get_version):
81+
request_context['request_id'] = 'abc123'
82+
app_name = 'gdal-subsetter'
83+
fake_lib_version = '0.1.0'
84+
get_version.return_value = fake_lib_version
85+
cfg = config_fixture(app_name=app_name)
86+
with patch('builtins.open', mock_open()):
87+
util.download('http://example/file.txt', 'tmp', access_token='', cfg=cfg)
88+
get.assert_called_with('http://example/file.txt?A-api-request-uuid=abc123', headers={'user-agent': f'harmony (unknown version) harmony-service-lib/{fake_lib_version} (gdal-subsetter)'}, timeout=60, stream=True)
89+
90+
@patch('harmony.util.get_version')
91+
@patch.object(Session, 'get')
92+
def test_https_download_sets_api_request_uuid(self, get, get_version):
93+
request_context['request_id'] = 'abc123'
94+
app_name = 'gdal-subsetter'
95+
fake_lib_version = '0.1.0'
96+
get_version.return_value = fake_lib_version
97+
cfg = config_fixture(app_name=app_name)
98+
with patch('builtins.open', mock_open()):
99+
util.download('https://example/file.txt', 'tmp', access_token='', cfg=cfg)
100+
get.assert_called_with('https://example/file.txt?A-api-request-uuid=abc123', headers={'user-agent': f'harmony (unknown version) harmony-service-lib/{fake_lib_version} (gdal-subsetter)'}, timeout=60, stream=True)
101+
102+
@patch('harmony.util.get_version')
103+
@patch.object(Session, 'post')
104+
def test_http_download_with_post_sets_api_request_uuid(self, post, get_version):
105+
request_context['request_id'] = 'abc123'
106+
app_name = 'gdal-subsetter'
107+
fake_lib_version = '0.1.0'
108+
get_version.return_value = fake_lib_version
109+
data = { 'foo': 'bar' }
110+
cfg = config_fixture(app_name=app_name)
111+
with patch('builtins.open', mock_open()):
112+
util.download('http://example/file.txt', 'tmp', access_token='', data=data, cfg=cfg)
113+
post.assert_called_with('http://example/file.txt?A-api-request-uuid=abc123', headers={'user-agent': f'harmony (unknown version) harmony-service-lib/{fake_lib_version} (gdal-subsetter)', 'Content-Type': 'application/x-www-form-urlencoded'}, data = { 'foo': 'bar' }, timeout=60, stream=True)
114+
115+
116+
@patch('harmony.util.get_version')
117+
@patch.object(Session, 'post')
118+
def test_https_download_with_post_sets_api_request_uuid(self, post, get_version):
119+
request_context['request_id'] = 'abc123'
120+
app_name = 'gdal-subsetter'
121+
fake_lib_version = '0.1.0'
122+
get_version.return_value = fake_lib_version
123+
data = { 'foo': 'bar' }
124+
cfg = config_fixture(app_name=app_name)
125+
with patch('builtins.open', mock_open()):
126+
util.download('https://example/file.txt', 'tmp', access_token='', data=data, cfg=cfg)
127+
post.assert_called_with('https://example/file.txt?A-api-request-uuid=abc123', headers={'user-agent': f'harmony (unknown version) harmony-service-lib/{fake_lib_version} (gdal-subsetter)', 'Content-Type': 'application/x-www-form-urlencoded'}, data = { 'foo': 'bar' }, timeout=60, stream=True)
128+
129+
61130
class TestStage(unittest.TestCase):
62131
def setUp(self):
63132
self.config = util.config(validate=False)

0 commit comments

Comments
 (0)