3
3
from multidict import CIMultiDict
4
4
import os
5
5
import re
6
+ import socket
6
7
from gettext import gettext as _
8
+ from functools import lru_cache
7
9
8
10
from aiohttp .client_exceptions import ClientResponseError
9
11
from aiohttp .web import FileResponse , StreamResponse , HTTPOk
21
23
22
24
import django
23
25
26
+ from opentelemetry import metrics
27
+
24
28
from pulpcore .constants import STORAGE_RESPONSE_MAP
25
29
from pulpcore .responses import ArtifactResponse
26
30
49
53
RemoteArtifact ,
50
54
)
51
55
from pulpcore .app import mime_types # noqa: E402: module level not at top of file
52
- from pulpcore .app .util import get_domain , cache_key # noqa: E402: module level not at top of file
56
+ from pulpcore .app .util import ( # noqa: E402: module level not at top of file
57
+ MetricsEmitter ,
58
+ get_domain ,
59
+ cache_key ,
60
+ )
53
61
54
62
from pulpcore .exceptions import UnsupportedDigestValidationError # noqa: E402
55
63
59
67
log = logging .getLogger (__name__ )
60
68
61
69
70
+ @lru_cache (maxsize = 1 )
71
+ def _get_content_app_name ():
72
+ return f"{ os .getpid ()} @{ socket .gethostname ()} "
73
+
74
+
62
75
class PathNotResolved (HTTPNotFound ):
63
76
"""
64
77
The path could not be resolved to a published file.
@@ -154,6 +167,20 @@ class Handler:
154
167
155
168
distribution_model = None
156
169
170
+ class ArtifactsSizeCounter (MetricsEmitter ):
171
+ def __init__ (self ):
172
+ self .meter = metrics .get_meter ("artifacts.size.meter" )
173
+ self .counter = self .meter .create_counter (
174
+ "artifacts.size.counter" ,
175
+ unit = "Bytes" ,
176
+ description = "Counts the size of served artifacts" ,
177
+ )
178
+
179
+ def add (self , amount , attributes ):
180
+ self .counter .add (amount , attributes )
181
+
182
+ artifacts_size_counter = ArtifactsSizeCounter .build ()
183
+
157
184
@staticmethod
158
185
def _reset_db_connection ():
159
186
"""
@@ -960,13 +987,37 @@ def _set_params_from_headers(hdrs, storage_domain):
960
987
params [STORAGE_RESPONSE_MAP [storage_domain ][a_key ]] = hdrs [a_key ]
961
988
return params
962
989
990
+ def _build_url (** kwargs ):
991
+ filename = os .path .basename (content_artifact .relative_path )
992
+ content_disposition = f"attachment;filename={ filename } "
993
+
994
+ headers ["Content-Disposition" ] = content_disposition
995
+ parameters = _set_params_from_headers (headers , domain .storage_class )
996
+ storage_url = storage .url (artifact_name , parameters = parameters , ** kwargs )
997
+
998
+ return URL (storage_url , encoded = True )
999
+
963
1000
artifact_file = content_artifact .artifact .file
964
1001
artifact_name = artifact_file .name
965
- filename = os .path .basename (content_artifact .relative_path )
966
- content_disposition = f"attachment;filename={ filename } "
967
1002
domain = get_domain ()
968
1003
storage = domain .get_storage ()
969
1004
1005
+ content_length = artifact_file .size
1006
+
1007
+ try :
1008
+ range_start , range_stop = request .http_range .start , request .http_range .stop
1009
+ if range_start or range_stop :
1010
+ if range_stop and artifact_file .size and range_stop > artifact_file .size :
1011
+ start = 0 if range_start is None else range_start
1012
+ content_length = artifact_file .size - start
1013
+ elif range_stop :
1014
+ content_length = range_stop - range_start
1015
+ except ValueError :
1016
+ size = artifact_file .size or "*"
1017
+ raise HTTPRequestRangeNotSatisfiable (headers = {"Content-Range" : f"bytes */{ size } " })
1018
+
1019
+ self ._report_served_artifact_size (content_length )
1020
+
970
1021
if domain .storage_class == "pulpcore.app.models.storage.FileSystem" :
971
1022
path = storage .path (artifact_name )
972
1023
if not os .path .exists (path ):
@@ -975,25 +1026,12 @@ def _set_params_from_headers(hdrs, storage_domain):
975
1026
elif not domain .redirect_to_object_storage :
976
1027
return ArtifactResponse (content_artifact .artifact , headers = headers )
977
1028
elif domain .storage_class == "storages.backends.s3boto3.S3Boto3Storage" :
978
- headers ["Content-Disposition" ] = content_disposition
979
- parameters = _set_params_from_headers (headers , domain .storage_class )
980
- url = URL (
981
- artifact_file .storage .url (
982
- artifact_name , parameters = parameters , http_method = request .method
983
- ),
984
- encoded = True ,
985
- )
986
- raise HTTPFound (url )
987
- elif domain .storage_class == "storages.backends.azure_storage.AzureStorage" :
988
- headers ["Content-Disposition" ] = content_disposition
989
- parameters = _set_params_from_headers (headers , domain .storage_class )
990
- url = URL (artifact_file .storage .url (artifact_name , parameters = parameters ), encoded = True )
991
- raise HTTPFound (url )
992
- elif domain .storage_class == "storages.backends.gcloud.GoogleCloudStorage" :
993
- headers ["Content-Disposition" ] = content_disposition
994
- parameters = _set_params_from_headers (headers , domain .storage_class )
995
- url = URL (artifact_file .storage .url (artifact_name , parameters = parameters ), encoded = True )
996
- raise HTTPFound (url )
1029
+ raise HTTPFound (_build_url (http_method = request .method ))
1030
+ elif domain .storage_class in (
1031
+ "storages.backends.azure_storage.AzureStorage" ,
1032
+ "storages.backends.gcloud.GoogleCloudStorage" ,
1033
+ ):
1034
+ raise HTTPFound (_build_url ())
997
1035
else :
998
1036
raise NotImplementedError ()
999
1037
@@ -1111,6 +1149,11 @@ async def finalize():
1111
1149
downloader .finalize = finalize
1112
1150
download_result = await downloader .run ()
1113
1151
1152
+ if content_length := response .headers .get ("Content-Length" ):
1153
+ self ._report_served_artifact_size (int (content_length ))
1154
+ else :
1155
+ self ._report_served_artifact_size (size )
1156
+
1114
1157
if save_artifact and remote .policy != Remote .STREAMED :
1115
1158
await asyncio .shield (
1116
1159
sync_to_async (self ._save_artifact )(download_result , remote_artifact , request )
@@ -1120,3 +1163,10 @@ async def finalize():
1120
1163
if response .status == 404 :
1121
1164
raise HTTPNotFound ()
1122
1165
return response
1166
+
1167
+ def _report_served_artifact_size (self , size ):
1168
+ attributes = {
1169
+ "domain_name" : get_domain ().name ,
1170
+ "content_app_name" : _get_content_app_name (),
1171
+ }
1172
+ self .artifacts_size_counter .add (size , attributes )
0 commit comments