33
44using System . Diagnostics ;
55using System . Diagnostics . Tracing ;
6+ using System . Dynamic ;
7+ using System . Net . Http . Json ;
68using System . Text . Json ;
79using Azure . Core ;
810using Azure . Core . Diagnostics ;
1214using Microsoft . DevProxy . Plugins . RequestLogs . ApiCenter ;
1315using Microsoft . Extensions . Configuration ;
1416using Microsoft . Extensions . Logging ;
17+ using Microsoft . OpenApi . Readers ;
1518
1619internal class ApiInformation
1720{
21+ public string Name { get ; set ; } = "" ;
1822 public ApiInformationVersion [ ] Versions { get ; set ; } = [ ] ;
19- // deployment.properties.server.runtimeUri[]
20- public string [ ] Urls { get ; set ; } = [ ] ;
2123}
2224
2325internal class ApiInformationVersion
2426{
25- public ApiInformationVersionInformation Version { get ; set ; } = new ( ) ;
26- public ApiLifecycleStage ? LifecycleStage { get ; set ; }
27- }
28-
29- internal class ApiInformationVersionInformation
30- {
31- // properties.title
27+ public string Title { get ; set ; } = "" ;
3228 public string Name { get ; set ; } = "" ;
33- // name
34- public string Id { get ; set ; } = "" ;
29+ public ApiLifecycleStage ? LifecycleStage { get ; set ; }
30+ public string [ ] Urls { get ; set ; } = [ ] ;
3531}
3632
3733internal class ApiCenterProductionVersionPluginConfiguration
@@ -154,44 +150,85 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
154150
155151 Debug . Assert ( _httpClient is not null ) ;
156152
157- var apis = await LoadApisFromApiCenter ( ) ;
158- if ( apis == null || ! apis . Value . Any ( ) )
153+ var apisFromApiCenter = await LoadApisFromApiCenter ( ) ;
154+ if ( apisFromApiCenter == null || ! apisFromApiCenter . Value . Any ( ) )
159155 {
160156 _logger ? . LogInformation ( "No APIs found in API Center" ) ;
161157 return ;
162158 }
163159
164160 var apisInformation = new List < ApiInformation > ( ) ;
165- foreach ( var api in apis . Value )
161+ foreach ( var api in apisFromApiCenter . Value )
166162 {
167- var apiVersions = await LoadApiVersions ( api ) ;
168- if ( apiVersions == null || ! apiVersions . Value . Any ( ) )
163+ var apiVersionsFromApiCenter = await LoadApiVersionsFromApiCenter ( api ) ;
164+ if ( apiVersionsFromApiCenter == null || ! apiVersionsFromApiCenter . Value . Any ( ) )
169165 {
170166 _logger ? . LogInformation ( "No versions found for {api}" , api . Properties ? . Title ) ;
171167 continue ;
172168 }
173169
174- var apiInformationVersion = apiVersions . Value . Select ( v => new ApiInformationVersion
170+ var versions = new List < ApiInformationVersion > ( ) ;
171+ foreach ( var versionFromApiCenter in apiVersionsFromApiCenter . Value )
175172 {
176- Version = new ApiInformationVersionInformation
173+ Debug . Assert ( versionFromApiCenter . Id is not null ) ;
174+
175+ var definitionsFromApiCenter = await LoadApiDefinitionsForVersion ( versionFromApiCenter . Id ) ;
176+ if ( definitionsFromApiCenter is null || ! definitionsFromApiCenter . Value . Any ( ) )
177+ {
178+ _logger ? . LogDebug ( "No definitions found for version {versionId}" , versionFromApiCenter . Id ) ;
179+ continue ;
180+ }
181+
182+ var apiUrls = new HashSet < string > ( ) ;
183+ foreach ( var definitionFromApiCenter in definitionsFromApiCenter . Value )
184+ {
185+ Debug . Assert ( definitionFromApiCenter . Id is not null ) ;
186+
187+ await EnsureApiDefinition ( definitionFromApiCenter ) ;
188+
189+ if ( definitionFromApiCenter . Definition is null )
190+ {
191+ _logger ? . LogDebug ( "API definition not found for {definitionId}" , definitionFromApiCenter . Id ) ;
192+ continue ;
193+ }
194+
195+ if ( ! definitionFromApiCenter . Definition . Servers . Any ( ) )
196+ {
197+ _logger ? . LogDebug ( "No servers found for API definition {definitionId}" , definitionFromApiCenter . Id ) ;
198+ continue ;
199+ }
200+
201+ foreach ( var server in definitionFromApiCenter . Definition . Servers )
202+ {
203+ apiUrls . Add ( server . Url ) ;
204+ }
205+ }
206+
207+ if ( ! apiUrls . Any ( ) )
177208 {
178- Name = v . Properties ? . Title ?? "" ,
179- Id = v . Id ?? ""
180- } ,
181- LifecycleStage = v . Properties ? . LifecycleStage
182- } ) . ToArray ( ) ;
183-
184- var apiDeployments = await LoadApiDeployments ( api ) ;
185- if ( apiDeployments == null || ! apiDeployments . Value . Any ( ) )
209+ _logger ? . LogDebug ( "No URLs found for version {versionId}" , versionFromApiCenter . Id ) ;
210+ continue ;
211+ }
212+
213+ versions . Add ( new ApiInformationVersion
214+ {
215+ Title = versionFromApiCenter . Properties ? . Title ?? "" ,
216+ Name = versionFromApiCenter . Name ?? "" ,
217+ LifecycleStage = versionFromApiCenter . Properties ? . LifecycleStage ,
218+ Urls = apiUrls . ToArray ( )
219+ } ) ;
220+ }
221+
222+ if ( ! versions . Any ( ) )
186223 {
187- _logger ? . LogInformation ( "No deployments found for {api}" , api . Properties ? . Title ) ;
224+ _logger ? . LogInformation ( "No versions found for {api}" , api . Properties ? . Title ) ;
188225 continue ;
189226 }
190227
191228 apisInformation . Add ( new ApiInformation
192229 {
193- Versions = apiInformationVersion ,
194- Urls = apiDeployments . Value . SelectMany ( d => d . Properties ? . Server ? . RuntimeUri ?? Array . Empty < string > ( ) ) . ToArray ( )
230+ Name = api . Properties ? . Title ?? "" ,
231+ Versions = versions . ToArray ( )
195232 } ) ;
196233 }
197234
@@ -218,25 +255,87 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
218255 {
219256 var productionVersions = apiInformation . Versions
220257 . Where ( v => v . LifecycleStage == ApiLifecycleStage . Production )
221- . Select ( v => v . Version . Name ) ;
258+ . Select ( v => v . Title ) ;
222259
223260 if ( productionVersions . Any ( ) )
224261 {
225- _logger ? . LogWarning ( "Request {request} uses API version {version} which is defined as {lifecycleStage}. Upgrade to a production version of the API. Recommended versions: {versions}" , urlAndMethodString , apiInformation . Versions . First ( v => v . LifecycleStage == lifecycleStage ) . Version . Name , lifecycleStage , string . Join ( ", " , productionVersions ) ) ;
262+ _logger ? . LogWarning ( "Request {request} uses API version {version} which is defined as {lifecycleStage}. Upgrade to a production version of the API. Recommended versions: {versions}" , urlAndMethodString , apiInformation . Versions . First ( v => v . LifecycleStage == lifecycleStage ) . Title , lifecycleStage , string . Join ( ", " , productionVersions ) ) ;
226263 }
227264 else
228265 {
229- _logger ? . LogWarning ( "Request {request} uses API version {version} which is defined as {lifecycleStage}." , urlAndMethodString , apiInformation . Versions . First ( v => v . LifecycleStage == lifecycleStage ) . Version . Name , lifecycleStage ) ;
266+ _logger ? . LogWarning ( "Request {request} uses API version {version} which is defined as {lifecycleStage}." , urlAndMethodString , apiInformation . Versions . First ( v => v . LifecycleStage == lifecycleStage ) . Title , lifecycleStage ) ;
230267 }
231268 }
232269 }
233270
234271 _logger ? . LogInformation ( "DONE" ) ;
235272 }
236273
274+ private async Task < Collection < ApiDefinition > ? > LoadApiDefinitionsForVersion ( string versionId )
275+ {
276+ Debug . Assert ( _httpClient is not null ) ;
277+
278+ _logger ? . LogDebug ( "Loading API definitions for version {id}..." , versionId ) ;
279+
280+ var res = await _httpClient . GetStringAsync ( $ "https://management.azure.com{ versionId } /definitions?api-version=2024-03-01") ;
281+ return JsonSerializer . Deserialize < Collection < ApiDefinition > > ( res , _jsonSerializerOptions ) ;
282+ }
283+
284+ async Task EnsureApiDefinition ( ApiDefinition apiDefinition )
285+ {
286+ Debug . Assert ( _httpClient is not null ) ;
287+
288+ if ( apiDefinition . Definition is not null )
289+ {
290+ _logger ? . LogDebug ( "API definition already loaded for {apiDefinitionId}" , apiDefinition . Id ) ;
291+ return ;
292+ }
293+
294+ _logger ? . LogDebug ( "Loading API definition for {apiDefinitionId}..." , apiDefinition . Id ) ;
295+
296+ var res = await _httpClient . GetStringAsync ( $ "https://management.azure.com{ apiDefinition . Id } ?api-version=2024-03-01") ;
297+ var definition = JsonSerializer . Deserialize < ApiDefinition > ( res , _jsonSerializerOptions ) ;
298+ if ( definition is null )
299+ {
300+ _logger ? . LogError ( "Failed to deserialize API definition for {apiDefinitionId}" , apiDefinition . Id ) ;
301+ return ;
302+ }
303+
304+ apiDefinition . Properties = definition . Properties ;
305+ if ( apiDefinition . Properties ? . Specification ? . Name != "openapi" )
306+ {
307+ _logger ? . LogDebug ( "API definition is not OpenAPI for {apiDefinitionId}" , apiDefinition . Id ) ;
308+ return ;
309+ }
310+
311+ var definitionRes = await _httpClient . PostAsync ( $ "https://management.azure.com{ apiDefinition . Id } /exportSpecification?api-version=2024-03-01", null ) ;
312+ var exportResult = await definitionRes . Content . ReadFromJsonAsync < ApiSpecExportResult > ( ) ;
313+ if ( exportResult is null )
314+ {
315+ _logger ? . LogError ( "Failed to deserialize exported API definition for {apiDefinitionId}" , apiDefinition . Id ) ;
316+ return ;
317+ }
318+
319+ if ( exportResult . Format != ApiSpecExportResultFormat . Inline )
320+ {
321+ _logger ? . LogDebug ( "API definition is not inline for {apiDefinitionId}" , apiDefinition . Id ) ;
322+ return ;
323+ }
324+
325+ try
326+ {
327+ apiDefinition . Definition = new OpenApiStringReader ( ) . Read ( exportResult . Value , out _ ) ;
328+ }
329+ catch ( Exception ex )
330+ {
331+ _logger ? . LogError ( ex , "Failed to parse OpenAPI document for {apiDefinitionId}" , apiDefinition . Id ) ;
332+ return ;
333+ }
334+ }
335+
237336 private ApiInformation ? FindMatchingApiInformation ( string requestUrl , List < ApiInformation > ? apisInformation )
238337 {
239- var apiInformation = apisInformation ? . FirstOrDefault ( a => a . Urls . Any ( u => requestUrl . StartsWith ( u ) ) ) ;
338+ var apiInformation = apisInformation ? . FirstOrDefault ( a => a . Versions . Any ( v => v . Urls . Any ( u => requestUrl . StartsWith ( u ) ) ) ) ;
240339 if ( apiInformation is null )
241340 {
242341 _logger ? . LogDebug ( "No matching API found for {request}" , requestUrl ) ;
@@ -258,22 +357,22 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
258357 foreach ( var apiVersion in apiInformation . Versions )
259358 {
260359 // check URL
261- if ( requestUrl . Contains ( apiVersion . Version . Id ) || requestUrl . Contains ( apiVersion . Version . Name ) )
360+ if ( requestUrl . Contains ( apiVersion . Name ) || requestUrl . Contains ( apiVersion . Title ) )
262361 {
263- _logger ? . LogDebug ( "Version {version} found in URL {url}" , $ "{ apiVersion . Version . Id } /{ apiVersion . Version . Name } ", requestUrl ) ;
362+ _logger ? . LogDebug ( "Version {version} found in URL {url}" , $ "{ apiVersion . Name } /{ apiVersion . Title } ", requestUrl ) ;
264363 version = apiVersion ;
265364 break ;
266365 }
267366
268367 // check headers
269368 Debug . Assert ( request . Context is not null ) ;
270369 var header = request . Context . Session . HttpClient . Request . Headers . FirstOrDefault (
271- h => h . Value . Contains ( apiVersion . Version . Id ) ||
272- h . Value . Contains ( apiVersion . Version . Name )
370+ h => h . Value . Contains ( apiVersion . Name ) ||
371+ h . Value . Contains ( apiVersion . Title )
273372 ) ;
274373 if ( header is not null )
275374 {
276- _logger ? . LogDebug ( "Version {version} found in header {header}" , $ "{ apiVersion . Version . Id } /{ apiVersion . Version . Name } ", header . Name ) ;
375+ _logger ? . LogDebug ( "Version {version} found in header {header}" , $ "{ apiVersion . Name } /{ apiVersion . Title } ", header . Name ) ;
277376 version = apiVersion ;
278377 break ;
279378 }
@@ -288,7 +387,7 @@ private async Task AfterRecordingStop(object sender, RecordingArgs e)
288387 return version . LifecycleStage ;
289388 }
290389
291- private async Task < Collection < ApiVersion > ? > LoadApiVersions ( Api api )
390+ private async Task < Collection < ApiVersion > ? > LoadApiVersionsFromApiCenter ( Api api )
292391 {
293392 Debug . Assert ( _httpClient is not null ) ;
294393
0 commit comments