diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj index 6f871b87c2c2..5b0121a2b4c9 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Azure.Iot.ModelsRepository.csproj @@ -35,5 +35,39 @@ + + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + Shared\Azure.Core + + + diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DependencyResolutionOption.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DependencyResolutionOption.cs index c1a52d404f24..5d570a5378de 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DependencyResolutionOption.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/DependencyResolutionOption.cs @@ -12,10 +12,12 @@ public enum DependencyResolutionOption /// Do not process external dependencies. /// Disabled, + /// /// Enable external dependencies. /// Enabled, + /// /// Try to get external dependencies using .expanded.json. /// diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs index 65cf7518c775..496062d3660a 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/LocalModelFetcher.cs @@ -8,15 +8,18 @@ using System.Collections.Generic; using System.Threading; using System.Globalization; +using Azure.Core.Pipeline; namespace Azure.Iot.ModelsRepository.Fetchers { internal class LocalModelFetcher : IModelFetcher { private readonly bool _tryExpanded; + private readonly ClientDiagnostics _clientDiagnostics; - public LocalModelFetcher(ResolverClientOptions clientOptions) + public LocalModelFetcher(ClientDiagnostics clientDiagnostics, ResolverClientOptions clientOptions) { + _clientDiagnostics = clientDiagnostics; _tryExpanded = clientOptions.DependencyResolution == DependencyResolutionOption.TryFromExpanded; } @@ -27,35 +30,48 @@ public Task FetchAsync(string dtmi, Uri repositoryUri, Cancellation public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) { - var work = new Queue(); + using DiagnosticScope scope = _clientDiagnostics.CreateScope("LocalModelFetcher.Fetch"); + scope.Start(); - if (_tryExpanded) + try { - work.Enqueue(GetPath(dtmi, repositoryUri, true)); - } + var work = new Queue(); - work.Enqueue(GetPath(dtmi, repositoryUri, false)); + if (_tryExpanded) + { + work.Enqueue(GetPath(dtmi, repositoryUri, true)); + } - string fnfError = string.Empty; - while (work.Count != 0 && !cancellationToken.IsCancellationRequested) - { - string tryContentPath = work.Dequeue(); - ResolverEventSource.Shared.FetchingModelContent(tryContentPath); + work.Enqueue(GetPath(dtmi, repositoryUri, false)); - if (File.Exists(tryContentPath)) + string fnfError = string.Empty; + while (work.Count != 0) { - return new FetchResult() + cancellationToken.ThrowIfCancellationRequested(); + + string tryContentPath = work.Dequeue(); + ResolverEventSource.Instance.FetchingModelContent(tryContentPath); + + if (File.Exists(tryContentPath)) { - Definition = File.ReadAllText(tryContentPath, Encoding.UTF8), - Path = tryContentPath - }; + return new FetchResult + { + Definition = File.ReadAllText(tryContentPath, Encoding.UTF8), + Path = tryContentPath + }; + } + + ResolverEventSource.Instance.ErrorFetchingModelContent(tryContentPath); + fnfError = string.Format(CultureInfo.CurrentCulture, ServiceStrings.ErrorFetchingModelContent, tryContentPath); } - ResolverEventSource.Shared.ErrorFetchingModelContent(tryContentPath); - fnfError = string.Format(CultureInfo.InvariantCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath); + throw new RequestFailedException(fnfError, new FileNotFoundException(fnfError)); + } + catch (Exception ex) + { + scope.Failed(ex); + throw; } - - throw new FileNotFoundException(fnfError); } private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = false) diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs index 8030a91ee0a7..31933d2c793d 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/Fetchers/RemoteModelFetcher.cs @@ -16,51 +16,110 @@ namespace Azure.Iot.ModelsRepository.Fetchers internal class RemoteModelFetcher : IModelFetcher { private readonly HttpPipeline _pipeline; + private readonly ClientDiagnostics _clientDiagnostics; private readonly bool _tryExpanded; - public RemoteModelFetcher(ResolverClientOptions clientOptions) + public RemoteModelFetcher(ClientDiagnostics clientDiagnostics, ResolverClientOptions clientOptions) { _pipeline = CreatePipeline(clientOptions); _tryExpanded = clientOptions.DependencyResolution == DependencyResolutionOption.TryFromExpanded; + _clientDiagnostics = clientDiagnostics; } public FetchResult Fetch(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) { - throw new NotImplementedException(); - } + using DiagnosticScope scope = _clientDiagnostics.CreateScope("RemoteModelFetcher.Fetch"); + scope.Start(); + try + { + Queue work = PrepareWork(dtmi, repositoryUri); - public async Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) - { - Queue work = new Queue(); + string remoteFetchError = string.Empty; - if (_tryExpanded) + while (work.Count != 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + string tryContentPath = work.Dequeue(); + ResolverEventSource.Instance.FetchingModelContent(tryContentPath); + + try + { + string content = EvaluatePath(tryContentPath, cancellationToken); + return new FetchResult + { + Definition = content, + Path = tryContentPath + }; + } + catch (Exception) + { + remoteFetchError = string.Format(CultureInfo.CurrentCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath); + } + } + + throw new RequestFailedException(remoteFetchError); + } + catch (Exception ex) { - work.Enqueue(GetPath(dtmi, repositoryUri, true)); + scope.Failed(ex); + throw; } + } - work.Enqueue(GetPath(dtmi, repositoryUri, false)); - - string remoteFetchError = string.Empty; - while (work.Count != 0 && !cancellationToken.IsCancellationRequested) + public async Task FetchAsync(string dtmi, Uri repositoryUri, CancellationToken cancellationToken = default) + { + using DiagnosticScope scope = _clientDiagnostics.CreateScope("RemoteModelFetcher.Fetch"); + scope.Start(); + try { - string tryContentPath = work.Dequeue(); - ResolverEventSource.Shared.FetchingModelContent(tryContentPath); + Queue work = PrepareWork(dtmi, repositoryUri); + + string remoteFetchError = string.Empty; - string content = await EvaluatePathAsync(tryContentPath, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrEmpty(content)) + while (work.Count != 0) { - return new FetchResult() + cancellationToken.ThrowIfCancellationRequested(); + + string tryContentPath = work.Dequeue(); + ResolverEventSource.Instance.FetchingModelContent(tryContentPath); + + try { - Definition = content, - Path = tryContentPath - }; + string content = await EvaluatePathAsync(tryContentPath, cancellationToken).ConfigureAwait(false); + return new FetchResult() + { + Definition = content, + Path = tryContentPath + }; + } + catch (Exception) + { + remoteFetchError = string.Format(CultureInfo.CurrentCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath); + } } - ResolverEventSource.Shared.ErrorFetchingModelContent(tryContentPath); - remoteFetchError = string.Format(CultureInfo.CurrentCulture, StandardStrings.ErrorFetchingModelContent, tryContentPath); + throw new RequestFailedException(remoteFetchError); + } + catch (Exception ex) + { + scope.Failed(ex); + throw; } + } + + private Queue PrepareWork(string dtmi, Uri repositoryUri) + { + Queue work = new Queue(); - throw new RequestFailedException(remoteFetchError); + if (_tryExpanded) + { + work.Enqueue(GetPath(dtmi, repositoryUri, true)); + } + + work.Enqueue(GetPath(dtmi, repositoryUri, false)); + + return work; } private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = false) @@ -69,32 +128,89 @@ private static string GetPath(string dtmi, Uri repositoryUri, bool expanded = fa return DtmiConventions.DtmiToQualifiedPath(dtmi, absoluteUri, expanded); } - private async Task EvaluatePathAsync(string path, CancellationToken cancellationToken) + private string EvaluatePath(string path, CancellationToken cancellationToken = default) { - Request request = _pipeline.CreateRequest(); - request.Method = RequestMethod.Get; - request.Uri = new RequestUriBuilder(); - request.Uri.Reset(new Uri(path)); + using DiagnosticScope scope = _clientDiagnostics.CreateScope("RemoteModelFetcher.EvaluatePath"); + scope.Start(); + + try + { + using HttpMessage message = CreateGetRequest(path); - Response response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); + _pipeline.Send(message, cancellationToken); - if (response.Status >= 200 && response.Status <= 299) + switch (message.Response.Status) + { + case 200: + { + return GetContent(message.Response.ContentStream); + } + default: + throw _clientDiagnostics.CreateRequestFailedException(message.Response); + } + } + catch (Exception ex) { - return await GetContentAsync(response.ContentStream, cancellationToken).ConfigureAwait(false); + scope.Failed(ex); + throw; } - - return null; } - private static async Task GetContentAsync(Stream content, CancellationToken cancellationToken) + private async Task EvaluatePathAsync(string path, CancellationToken cancellationToken = default) { - using (JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false)) + using DiagnosticScope scope = _clientDiagnostics.CreateScope("RemoteModelFetcher.EvaluatePath"); + scope.Start(); + + try + { + using HttpMessage message = CreateGetRequest(path); + + await _pipeline.SendAsync(message, cancellationToken).ConfigureAwait(false); + + switch (message.Response.Status) + { + case 200: + { + return await GetContentAsync(message.Response.ContentStream, cancellationToken).ConfigureAwait(false); + } + default: + throw _clientDiagnostics.CreateRequestFailedException(message.Response); + } + } + catch (Exception ex) { - JsonElement root = json.RootElement; - return root.GetRawText(); + scope.Failed(ex); + throw; } } + private HttpMessage CreateGetRequest(string path) + { + HttpMessage message = _pipeline.CreateMessage(); + Request request = message.Request; + request.Method = RequestMethod.Get; + var uri = new RequestUriBuilder(); + uri.Reset(new Uri(path)); + request.Uri = uri; + + return message; + } + + private static string GetContent(Stream content) + { + using JsonDocument json = JsonDocument.Parse(content); + JsonElement root = json.RootElement; + return root.GetRawText(); + } + + private static async Task GetContentAsync(Stream content, CancellationToken cancellationToken) + { + using JsonDocument json = await JsonDocument.ParseAsync(content, default, cancellationToken).ConfigureAwait(false); + + JsonElement root = json.RootElement; + return root.GetRawText(); + } + private static HttpPipeline CreatePipeline(ResolverClientOptions options) { return HttpPipelineBuilder.Build(options); diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs index b8c8d68be760..7782dcaa7611 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/RepositoryHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using Azure.Core.Pipeline; using Azure.Iot.ModelsRepository.Fetchers; using System; using System.Collections.Generic; @@ -14,19 +15,21 @@ internal class RepositoryHandler { private readonly IModelFetcher _modelFetcher; private readonly Guid _clientId; + private readonly ClientDiagnostics _clientDiagnostics; public Uri RepositoryUri { get; } public ResolverClientOptions ClientOptions { get; } - public RepositoryHandler(Uri repositoryUri, ResolverClientOptions options = null) + public RepositoryHandler(Uri repositoryUri, ClientDiagnostics clientdiagnostics, ResolverClientOptions options = null) { ClientOptions = options ?? new ResolverClientOptions(); RepositoryUri = repositoryUri; + _clientDiagnostics = clientdiagnostics; _modelFetcher = repositoryUri.Scheme == "file" - ? _modelFetcher = new LocalModelFetcher(ClientOptions) - : _modelFetcher = new RemoteModelFetcher(ClientOptions); + ? _modelFetcher = new LocalModelFetcher(_clientDiagnostics, ClientOptions) + : _modelFetcher = new RemoteModelFetcher(_clientDiagnostics, ClientOptions); _clientId = Guid.NewGuid(); - ResolverEventSource.Shared.InitFetcher(_clientId, repositoryUri.Scheme); + ResolverEventSource.Instance.InitFetcher(_clientId, repositoryUri.Scheme); } public async Task> ProcessAsync(string dtmi, CancellationToken cancellationToken) @@ -43,7 +46,7 @@ public async Task> ProcessAsync(IEnumerable { if (!DtmiConventions.IsDtmi(dtmi)) { - ResolverEventSource.Shared.InvalidDtmiInput(dtmi); + ResolverEventSource.Instance.InvalidDtmiInput(dtmi); string invalidArgMsg = string.Format(CultureInfo.CurrentCulture, ServiceStrings.InvalidDtmiFormat, dtmi); throw new ResolverException(dtmi, invalidArgMsg, new ArgumentException(invalidArgMsg)); } @@ -56,10 +59,10 @@ public async Task> ProcessAsync(IEnumerable string targetDtmi = toProcessModels.Dequeue(); if (processedModels.ContainsKey(targetDtmi)) { - ResolverEventSource.Shared.SkippingPreprocessedDtmi(targetDtmi); + ResolverEventSource.Instance.SkippingPreprocessedDtmi(targetDtmi); continue; } - ResolverEventSource.Shared.ProcessingDtmi(targetDtmi); + ResolverEventSource.Instance.ProcessingDtmi(targetDtmi); FetchResult result = await FetchAsync(targetDtmi, cancellationToken).ConfigureAwait(false); if (result.FromExpanded) @@ -85,7 +88,7 @@ public async Task> ProcessAsync(IEnumerable if (dependencies.Count > 0) { - ResolverEventSource.Shared.DiscoveredDependencies(string.Join("\", \"", dependencies)); + ResolverEventSource.Instance.DiscoveredDependencies(string.Join("\", \"", dependencies)); } foreach (string dep in dependencies) @@ -97,7 +100,7 @@ public async Task> ProcessAsync(IEnumerable string parsedDtmi = metadata.Id; if (!parsedDtmi.Equals(targetDtmi, StringComparison.Ordinal)) { - ResolverEventSource.Shared.IncorrectDtmiCasing(targetDtmi, parsedDtmi); + ResolverEventSource.Instance.IncorrectDtmiCasing(targetDtmi, parsedDtmi); string formatErrorMsg = string.Format(CultureInfo.CurrentCulture, ServiceStrings.IncorrectDtmiCasing, targetDtmi, parsedDtmi); throw new ResolverException(targetDtmi, formatErrorMsg, new FormatException(formatErrorMsg)); } diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs index a3f6e896758c..5fbc02ff2b52 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverClient.cs @@ -6,6 +6,8 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; +using Azure.Core; +using Azure.Core.Pipeline; namespace Azure.Iot.ModelsRepository { @@ -17,12 +19,13 @@ public class ResolverClient { internal const string DefaultRepository = "https://devicemodels.azure.com"; private readonly RepositoryHandler _repositoryHandler; + private readonly ClientDiagnostics _clientDiagnostics; /// /// Initializes the ResolverClient with default client options while pointing to /// the Azure IoT Plug and Play Model repository https://devicemodels.azure.com for resolution. /// - public ResolverClient() : this(new Uri(DefaultRepository), null) { } + public ResolverClient() : this(new Uri(DefaultRepository), new ResolverClientOptions()) { } /// /// Initializes the ResolverClient with default client options while pointing to @@ -31,7 +34,7 @@ public ResolverClient() : this(new Uri(DefaultRepository), null) { } /// /// The model repository Uri value. This can be a remote endpoint or local directory. /// - public ResolverClient(Uri repositoryUri) : this(repositoryUri, null) { } + public ResolverClient(Uri repositoryUri) : this(repositoryUri, new ResolverClientOptions()) { } /// /// Initializes the ResolverClient with custom client while pointing to @@ -42,21 +45,6 @@ public ResolverClient(Uri repositoryUri) : this(repositoryUri, null) { } /// public ResolverClient(ResolverClientOptions options) : this(new Uri(DefaultRepository), options) { } - /// - /// Initializes the ResolverClient with custom client while pointing to - /// a custom for resolution. - /// - /// - /// The model repository Uri. This can be a remote endpoint or local directory. - /// - /// - /// ResolverClientOptions to configure resolution and client behavior. - /// - public ResolverClient(Uri repositoryUri, ResolverClientOptions options) - { - _repositoryHandler = new RepositoryHandler(repositoryUri, options); - } - /// /// Initializes the ResolverClient with default client options while pointing to /// a custom for resolution. @@ -64,7 +52,7 @@ public ResolverClient(Uri repositoryUri, ResolverClientOptions options) /// /// The model repository Uri in string format. This can be a remote endpoint or local directory. /// - public ResolverClient(string repositoryUriStr) : this(repositoryUriStr, null) { } + public ResolverClient(string repositoryUriStr) : this(repositoryUriStr, new ResolverClientOptions()) { } /// /// Initializes the ResolverClient with custom client while pointing to @@ -79,6 +67,24 @@ public ResolverClient(string repositoryUriStr) : this(repositoryUriStr, null) { public ResolverClient(string repositoryUriStr, ResolverClientOptions options) : this(new Uri(repositoryUriStr), options) { } + /// + /// Initializes the ResolverClient with custom client while pointing to + /// a custom for resolution. + /// + /// + /// The model repository Uri. This can be a remote endpoint or local directory. + /// + /// + /// ResolverClientOptions to configure resolution and client behavior. + /// + public ResolverClient(Uri repositoryUri, ResolverClientOptions options) + { + Argument.AssertNotNull(options, nameof(options)); + + _clientDiagnostics = new ClientDiagnostics(options); + _repositoryHandler = new RepositoryHandler(repositoryUri, _clientDiagnostics, options); + } + /// /// Resolves a model definition identified by and optionally its dependencies. /// diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs index 56bfbf5789e2..a9b84c3ae67e 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/src/ResolverEventSource.cs @@ -13,6 +13,10 @@ internal sealed class ResolverEventSource : EventSource private const string EventSourceName = ModelRepositoryConstants.ModelRepositoryEventSourceName; // Event ids defined as constants to makes it easy to keep track of them + // Consider EventSource name, Guid, Event Id and parameters as public API and follow the appropriate versioning rules. + // More information on EventSource and Azure guidelines: + // https://azure.github.io/azure-sdk/dotnet_implementation.html#eventsource + private const int InitFetcherEventId = 1000; private const int ProcessingDtmiEventId = 2000; private const int FetchingModelContentEventId = 2001; @@ -22,7 +26,7 @@ internal sealed class ResolverEventSource : EventSource private const int ErrorFetchingModelContentEventId = 4004; private const int IncorrectDtmiCasingEventId = 4006; - public static ResolverEventSource Shared { get; } = new ResolverEventSource(); + public static ResolverEventSource Instance { get; } = new ResolverEventSource(); private ResolverEventSource() : base(EventSourceName, diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs index 78a2102d07f5..aff871d3c72c 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/ClientTests.cs @@ -24,11 +24,9 @@ public void CtorOverloads() Assert.AreEqual(remoteUri, new ResolverClient(remoteUri).RepositoryUri); Assert.AreEqual(remoteUri, new ResolverClient(remoteUri, options).RepositoryUri); - Assert.AreEqual(remoteUri, new ResolverClient(remoteUri, null).RepositoryUri); Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr).RepositoryUri); Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr, options).RepositoryUri); - Assert.AreEqual(remoteUri, new ResolverClient(remoteUriStr, null).RepositoryUri); string localUriStr = TestHelpers.TestLocalModelRepository; Uri localUri = new Uri(localUriStr); diff --git a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs index a77011f35f10..f1d0297e832b 100644 --- a/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs +++ b/sdk/modelsrepository/Azure.Iot.ModelsRepository/tests/TestHelpers.cs @@ -35,6 +35,11 @@ public static string ParseRootDtmiFromJson(string json) public static ResolverClient GetTestClient(ClientType clientType, ResolverClientOptions clientOptions = null) { + if (clientOptions == null) + { + clientOptions = new ResolverClientOptions(); + } + if (clientType == ClientType.Local) { return new ResolverClient(TestLocalModelRepository, clientOptions);