44import urllib .parse
55from base64 import b64encode
66from decimal import Decimal
7+ from enum import Enum
78from io import StringIO
8- from typing import Optional , Tuple , Pattern
9+ from typing import Dict , Optional , Pattern , Tuple
910
1011import coverage
1112import requests
1617log = logging .getLogger ("codecovopentelem" )
1718
1819
20+ class CoverageSpanFilter (Enum ):
21+ regex_name_filter = "name_regex"
22+ span_kind_filter = "span_kind"
23+
24+
25+ class UnableToStartProcessorException (Exception ):
26+ pass
27+
28+
1929class CodecovCoverageStorageManager (object ):
20- def __init__ (self , writeable_folder : str ):
30+ def __init__ (self , writeable_folder : str , filters : Dict ):
2131 if writeable_folder is None :
2232 writeable_folder = "/home/codecov"
2333 self ._writeable_folder = writeable_folder
2434 self .inner = {}
35+ self ._filters = filters
2536
26- def start_cov_for_span (self , span_id ):
37+ def possibly_start_cov_for_span (self , span ) -> bool :
38+ span_id = span .context .span_id
39+ if self ._filters .get (
40+ CoverageSpanFilter .regex_name_filter
41+ ) and not self ._filters .get (CoverageSpanFilter .regex_name_filter ).match (
42+ span .name
43+ ):
44+ return False
45+ if self ._filters .get (
46+ CoverageSpanFilter .span_kind_filter
47+ ) and span .kind not in self ._filters .get (CoverageSpanFilter .span_kind_filter ):
48+ return False
2749 cov = coverage .Coverage (data_file = f"{ self ._writeable_folder } /.{ span_id } file" )
2850 self .inner [span_id ] = cov
2951 cov .start ()
52+ return True
3053
31- def stop_cov_for_span (self , span_id ):
54+ def stop_cov_for_span (self , span ):
55+ span_id = span .context .span_id
3256 cov = self .inner .get (span_id )
3357 if cov is not None :
3458 cov .stop ()
3559
36- def pop_cov_for_span (self , span_id ):
60+ def pop_cov_for_span (self , span ):
61+ span_id = span .context .span_id
3762 return self .inner .pop (span_id , None )
3863
3964
4065class CodecovCoverageGenerator (SpanProcessor ):
4166 def __init__ (
42- self ,
43- cov_storage : CodecovCoverageStorageManager ,
44- sample_rate : Decimal ,
45- name_regex : Pattern = None ,
67+ self , cov_storage : CodecovCoverageStorageManager , sample_rate : Decimal ,
4668 ):
4769 self ._cov_storage = cov_storage
4870 self ._sample_rate = sample_rate
49- self ._name_regex = name_regex
5071
5172 def _should_profile_span (self , span , parent_context ):
52- return random .random () < self ._sample_rate and (
53- self ._name_regex is None or self ._name_regex .match (span .name )
54- )
73+ return random .random () < self ._sample_rate
5574
5675 def on_start (self , span , parent_context = None ):
5776 if self ._should_profile_span (span , parent_context ):
58- span_id = span .context .span_id
59- self ._cov_storage .start_cov_for_span (span_id )
77+ self ._cov_storage .possibly_start_cov_for_span (span )
6078
6179 def on_end (self , span ):
62- span_id = span .context .span_id
63- self ._cov_storage .stop_cov_for_span (span_id )
80+ self ._cov_storage .stop_cov_for_span (span )
6481
6582
6683class CoverageExporter (SpanExporter ):
6784 def __init__ (
6885 self ,
6986 cov_storage : CodecovCoverageStorageManager ,
7087 repository_token : str ,
71- profiling_identifier : str ,
88+ profiling_id : str ,
7289 codecov_endpoint : str ,
7390 untracked_export_rate : float ,
7491 ):
7592 self ._cov_storage = cov_storage
7693 self ._repository_token = repository_token
77- self ._profiling_identifier = profiling_identifier
94+ self ._profiling_id = profiling_id
7895 self ._codecov_endpoint = codecov_endpoint
7996 self ._untracked_export_rate = untracked_export_rate
8097
@@ -96,71 +113,98 @@ def export(self, spans):
96113 tracked_spans = []
97114 untracked_spans = []
98115 for span in spans :
99- span_id = span .context .span_id
100- cov = self ._cov_storage .pop_cov_for_span (span_id )
116+ cov = self ._cov_storage .pop_cov_for_span (span )
101117 s = json .loads (span .to_json ())
102118 if cov is not None :
103119 s ["codecov" ] = self ._load_codecov_dict (span , cov )
104120 tracked_spans .append (s )
105121 else :
106122 if random .random () < self ._untracked_export_rate :
107123 untracked_spans .append (s )
108- if not tracked_spans :
124+ if not tracked_spans and not untracked_spans :
109125 return SpanExportResult .SUCCESS
110126 url = urllib .parse .urljoin (self ._codecov_endpoint , "/profiling/uploads" )
111- res = requests .post (
112- url ,
113- headers = {"Authorization" : f"repotoken { self ._repository_token } " },
114- json = {"profiling" : self ._profiling_identifier },
115- )
116127 try :
128+ res = requests .post (
129+ url ,
130+ headers = {"Authorization" : f"repotoken { self ._repository_token } " },
131+ json = {"profiling" : self ._profiling_id },
132+ )
117133 res .raise_for_status ()
118- except requests .HTTPError :
134+ except requests .RequestException :
119135 log .warning ("Unable to send profiling data to codecov" )
120136 return SpanExportResult .FAILURE
121137 location = res .json ()["raw_upload_location" ]
122138 requests .put (
123139 location ,
124140 headers = {"Content-Type" : "application/txt" },
125- data = json .dumps ({"spans" : tracked_spans , "untracked" : untracked_spans }).encode (),
141+ data = json .dumps (
142+ {"spans" : tracked_spans , "untracked" : untracked_spans }
143+ ).encode (),
126144 )
127145 return SpanExportResult .SUCCESS
128146
129147
130148def get_codecov_opentelemetry_instances (
131149 repository_token : str ,
132- profiling_identifier : str ,
133150 sample_rate : float ,
134- name_regex : Optional [Pattern ],
151+ untracked_export_rate : float ,
152+ filters : Optional [Dict ] = None ,
153+ profiling_identifier : Optional [str ] = None ,
154+ environment : Optional [str ] = None ,
155+ profiling_id : Optional [str ] = None ,
135156 codecov_endpoint : str = None ,
136157 writeable_folder : str = None ,
137158) -> Tuple [CodecovCoverageGenerator , CoverageExporter ]:
138159 """
139160 Entrypoint for getting a span processor/span exporter
140161 pair for getting profiling data into codecov
141162
163+ Notice that either `profiling_id` or `profiling_identifier` and `environment` need to be set.
164+ If `profiling_id` is set, we just use it directly on the exporter. If not, we will use
165+ `profiling_identifier` and `environment` to generate fetch a `profiling_id` from the
166+ database
167+
142168 Args:
143169 repository_token (str): The profiling-capable authentication token
144- profiling_identifier (str): The identifier for what profiling one is doing
145170 sample_rate (float): The sampling rate for codecov
146- name_regex (Optional[Pattern]): A regex to filter which spans should be
147- sampled
171+ untracked_export_rate (float): Description
172+ filters (Optional[Dict], optional): A dictionary of filters for determining which
173+ spans should have its coverage tracked
174+ profiling_identifier (Optional[str], optional): The identifier for what profiling one is doing
175+ environment (Optional[str], optional): Which environment this profiling is running on
176+ profiling_id (Optional[str], optional): Description
148177 codecov_endpoint (str, optional): For configuring the endpoint in case
149178 the user is in enterprise (not supported yet). Default is "https://api.codecov.io/"
150179 writeable_folder (str, optional): A folder that is guaranteed to be write-able
151180 in the system. It's only used for temporary files, and nothing is expected
152181 to live very long in there.
153182 """
154- if codecov_endpoint is None :
155- codecov_endpoint = "https://api.codecov.io"
156- manager = CodecovCoverageStorageManager (writeable_folder )
157- generator = CodecovCoverageGenerator (manager , sample_rate , name_regex )
158- # untracked rate set to make it so we export roughly as many tracked and untracked spans
159- untracked_export_rate = sample_rate / (1 - sample_rate ) if sample_rate < 1 else 0
183+ codecov_endpoint = codecov_endpoint or "https://api.codecov.io"
184+ if profiling_id is None :
185+ if profiling_identifier is None or environment is None :
186+ raise UnableToStartProcessorException (
187+ "Codecov profiling needs either the id or identifier + environment"
188+ )
189+ response = requests .post (
190+ urllib .parse .urljoin (codecov_endpoint , "/profiling/versions" ),
191+ json = {
192+ "version_identifier" : profiling_identifier ,
193+ "environment" : environment ,
194+ },
195+ headers = {"Authorization" : f"repotoken { repository_token } " },
196+ )
197+ try :
198+ response .raise_for_status ()
199+ except requests .HTTPError :
200+ raise UnableToStartProcessorException ()
201+ profiling_id = response .json ()["external_id" ]
202+ manager = CodecovCoverageStorageManager (writeable_folder , filters or {})
203+ generator = CodecovCoverageGenerator (manager , sample_rate )
160204 exporter = CoverageExporter (
161205 manager ,
162206 repository_token ,
163- profiling_identifier ,
207+ profiling_id ,
164208 codecov_endpoint ,
165209 untracked_export_rate ,
166210 )
0 commit comments