diff --git a/aspire/AppHost.cs b/aspire/AppHost.cs index 6287569def..e89cdc29f1 100644 --- a/aspire/AppHost.cs +++ b/aspire/AppHost.cs @@ -61,7 +61,7 @@ internal static async Task Run( var elasticsearchRemote = builder.AddExternalService(ElasticsearchRemote, elasticsearchUrl); - var api = builder.AddProject(Api) + var api = builder.AddProject(Api) .WithArgs(GlobalArguments) .WithEnvironment("ENVIRONMENT", "dev") .WithEnvironment("LLM_GATEWAY_FUNCTION_URL", llmUrl) diff --git a/aspire/aspire.csproj b/aspire/aspire.csproj index 5c1073d0e9..5bc3691a3f 100644 --- a/aspire/aspire.csproj +++ b/aspire/aspire.csproj @@ -20,7 +20,7 @@ - + diff --git a/docs-builder.slnx b/docs-builder.slnx index acbab48b36..12215d0721 100644 --- a/docs-builder.slnx +++ b/docs-builder.slnx @@ -53,12 +53,11 @@ + - - - + @@ -73,7 +72,10 @@ - + + + + diff --git a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs index 8c17cc0223..38a90d901e 100644 --- a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs +++ b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs @@ -11,6 +11,7 @@ using Elastic.Documentation.Configuration.Inference; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Search; +using Elastic.Documentation.Versions; using Microsoft.OpenApi; using Microsoft.OpenApi.Reader; @@ -177,7 +178,7 @@ private IEnumerable ConvertToDocuments(OpenApiDocument op StrippedBody = body, Headings = headings, Links = [], - Applies = applies, + Applies = applies?.ToAppliesTo(), Parents = [ new ParentDocument { Title = "API Reference", Url = "/docs/api" }, diff --git a/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs b/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs index bd1b0cb869..ba8cbf3cdd 100644 --- a/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs +++ b/src/Elastic.ApiExplorer/Operations/AvailabilityBadgeHelper.cs @@ -8,6 +8,7 @@ using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Versions; using Microsoft.OpenApi; namespace Elastic.ApiExplorer.Operations; diff --git a/src/Elastic.Codex/Building/CodexBuildService.cs b/src/Elastic.Codex/Building/CodexBuildService.cs index 5d2efb8222..8a5ba281d0 100644 --- a/src/Elastic.Codex/Building/CodexBuildService.cs +++ b/src/Elastic.Codex/Building/CodexBuildService.cs @@ -10,6 +10,7 @@ using Elastic.Codex.Sourcing; using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Configuration.Codex; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Isolated; diff --git a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs index 133ab98db6..f9833a0772 100644 --- a/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Extensions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using YamlDotNet.Serialization; diff --git a/src/Elastic.Documentation.Configuration/BuildContext.cs b/src/Elastic.Documentation.Configuration/BuildContext.cs index 28cf823d1e..5ea7d914a9 100644 --- a/src/Elastic.Documentation.Configuration/BuildContext.cs +++ b/src/Elastic.Documentation.Configuration/BuildContext.cs @@ -117,7 +117,7 @@ public BuildContext( if (ConfigurationPath.FullName != DocumentationSourceDirectory.FullName) DocumentationSourceDirectory = ConfigurationPath.Directory!; - Git = gitCheckoutInformation ?? GitCheckoutInformation.Create(DocumentationCheckoutDirectory, ReadFileSystem); + Git = gitCheckoutInformation ?? GitCheckoutInformationFactory.Create(DocumentationCheckoutDirectory, ReadFileSystem); // Load and resolve the docset file, or create an empty one if it doesn't exist ConfigurationYaml = ConfigurationPath.Exists diff --git a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs index d3a0b8eec8..f20055f83b 100644 --- a/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs @@ -11,7 +11,7 @@ using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Extensions; using Elastic.Documentation.Links; -using static Elastic.Documentation.Configuration.SymlinkValidator; +using static Elastic.Documentation.SymlinkValidator; namespace Elastic.Documentation.Configuration.Builder; diff --git a/src/Elastic.Documentation.Configuration/CrossLinkEntry.cs b/src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs similarity index 91% rename from src/Elastic.Documentation.Configuration/CrossLinkEntry.cs rename to src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs index 9dc66af13d..88e2e591d4 100644 --- a/src/Elastic.Documentation.Configuration/CrossLinkEntry.cs +++ b/src/Elastic.Documentation.Configuration/Builder/CrossLinkEntry.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation.Configuration.Builder; /// /// A parsed cross-link entry from docset.yml, with the target registry for lookup. diff --git a/src/Elastic.Documentation.Configuration/DocSetRegistry.cs b/src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs similarity index 93% rename from src/Elastic.Documentation.Configuration/DocSetRegistry.cs rename to src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs index cfcf9bbf23..ae06ae372d 100644 --- a/src/Elastic.Documentation.Configuration/DocSetRegistry.cs +++ b/src/Elastic.Documentation.Configuration/Builder/DocSetRegistry.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation.Configuration.Builder; /// /// Registry type for cross-link resolution. Maps to the link index source: diff --git a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs index ca43dadb84..ebb6a12597 100644 --- a/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs +++ b/src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs @@ -5,7 +5,7 @@ using System.IO.Abstractions; using Elastic.Documentation.Links; using YamlDotNet.RepresentationModel; -using static Elastic.Documentation.Configuration.SymlinkValidator; +using static Elastic.Documentation.SymlinkValidator; namespace Elastic.Documentation.Configuration.Builder; diff --git a/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs index 6b52229a0a..f24c55019c 100644 --- a/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfiguration.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Documentation.Configuration.Products; +using Elastic.Documentation.ReleaseNotes; namespace Elastic.Documentation.Configuration.Changelog; diff --git a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs similarity index 99% rename from src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs rename to src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs index 4c866be716..38a73ee728 100644 --- a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationLoader.cs @@ -3,11 +3,10 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; -using Elastic.Changelog.Serialization; using Elastic.Documentation; using Elastic.Documentation.Configuration; -using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.Products; +using Elastic.Documentation.Configuration.Serialization; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; using Microsoft.Extensions.Logging; @@ -15,7 +14,7 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace Elastic.Changelog.Configuration; +namespace Elastic.Documentation.Configuration.Changelog; /// /// Service for loading and validating changelog configuration @@ -25,7 +24,7 @@ public class ChangelogConfigurationLoader(ILoggerFactory logFactory, IConfigurat private readonly ILogger _logger = logFactory.CreateLogger(); private static readonly IDeserializer ConfigurationDeserializer = - new StaticDeserializerBuilder(new ChangelogYamlStaticContext()) + new StaticDeserializerBuilder(new YamlStaticContext()) .WithNamingConvention(UnderscoredNamingConvention.Instance) .WithTypeConverter(new YamlLenientListConverter()) .WithTypeConverter(new TypeEntryYamlConverter()) diff --git a/src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs similarity index 95% rename from src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs rename to src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs index 03c56e9b8e..80521f6742 100644 --- a/src/services/Elastic.Changelog/Serialization/ChangelogConfigurationYaml.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/ChangelogConfigurationYaml.cs @@ -2,13 +2,13 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// Internal DTO for YAML deserialization of changelog configuration. /// Maps directly to the YAML file structure. /// -internal record ChangelogConfigurationYaml +internal sealed record ChangelogConfigurationYaml { /// /// Filename strategy for generated changelog files (pr, issue, timestamp). @@ -57,7 +57,7 @@ internal record ChangelogConfigurationYaml /// /// Internal DTO for rules configuration in YAML. /// -internal record RulesConfigurationYaml +internal sealed record RulesConfigurationYaml { /// /// Global match mode for multi-valued fields ("any" or "all"). Default: "any". @@ -83,7 +83,7 @@ internal record RulesConfigurationYaml /// /// Internal DTO for bundle rules in YAML. /// -internal record BundleRulesYaml +internal sealed record BundleRulesYaml { /// /// Product IDs to exclude from the bundle (string or list). Cannot be combined with IncludeProducts. @@ -134,7 +134,7 @@ internal record BundleRulesYaml /// /// Internal DTO for create rules in YAML. /// -internal record CreateRulesYaml +internal sealed record CreateRulesYaml { /// /// Labels to exclude (string or list). Cannot be combined with Include. @@ -161,7 +161,7 @@ internal record CreateRulesYaml /// /// Internal DTO for publish rules in YAML. /// -internal record PublishRulesYaml +internal sealed record PublishRulesYaml { /// /// Match mode for areas ("any" or "all"). Inherits from rules.match if not set. @@ -198,7 +198,7 @@ internal record PublishRulesYaml /// /// Internal DTO for pivot configuration in YAML. /// -internal record PivotConfigurationYaml +internal sealed record PivotConfigurationYaml { /// /// Type definitions with optional labels and subtypes. @@ -231,7 +231,7 @@ internal record PivotConfigurationYaml /// /// Internal DTO for products configuration in YAML. /// -internal record ProductsConfigYaml +internal sealed record ProductsConfigYaml { /// /// List of available product IDs (string or list, empty = all from products.yml). @@ -247,7 +247,7 @@ internal record ProductsConfigYaml /// /// Internal DTO for default product specification in YAML. /// -internal record DefaultProductYaml +internal sealed record DefaultProductYaml { /// /// Product ID. @@ -263,7 +263,7 @@ internal record DefaultProductYaml /// /// Internal DTO for bundle configuration in YAML. /// -internal record BundleConfigurationYaml +internal sealed record BundleConfigurationYaml { /// /// Input directory containing changelog YAML files. @@ -314,7 +314,7 @@ internal record BundleConfigurationYaml /// /// Internal DTO for bundle profile in YAML. /// -internal record BundleProfileYaml +internal sealed record BundleProfileYaml { /// /// Product filter pattern for input changelogs. @@ -371,7 +371,7 @@ internal record BundleProfileYaml /// /// Internal DTO for extract configuration in YAML. /// -internal record ExtractConfigurationYaml +internal sealed record ExtractConfigurationYaml { /// /// Whether to extract release note text from PR or issue descriptions for the changelog entry description by default. @@ -397,7 +397,7 @@ internal record ExtractConfigurationYaml /// Internal DTO for type entry in YAML. /// Can represent either a simple label string or an object with labels and subtypes. /// -internal record TypeEntryYaml +internal sealed record TypeEntryYaml { /// /// Labels for this type (comma-separated string). diff --git a/src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs b/src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs similarity index 98% rename from src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs rename to src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs index 57c328b24f..135a5ffd78 100644 --- a/src/services/Elastic.Changelog/Serialization/TypeEntryYamlConverter.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/TypeEntryYamlConverter.cs @@ -6,7 +6,7 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// YAML type converter for TypeEntryYaml that handles both string and object forms. diff --git a/src/services/Elastic.Changelog/Serialization/YamlLenientList.cs b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs similarity index 86% rename from src/services/Elastic.Changelog/Serialization/YamlLenientList.cs rename to src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs index 724f8538fb..58c77ce908 100644 --- a/src/services/Elastic.Changelog/Serialization/YamlLenientList.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientList.cs @@ -2,14 +2,14 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// Wrapper type for YAML fields that can be specified as either a comma-separated string /// or a YAML list/sequence. Deserialized by . /// Uses mutable property for compatibility with YamlDotNet source generator. /// -internal class YamlLenientList +internal sealed class YamlLenientList { public List? Values { get; set; } diff --git a/src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs similarity index 98% rename from src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs rename to src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs index 49bbbb2d53..8facb13be3 100644 --- a/src/services/Elastic.Changelog/Serialization/YamlLenientListConverter.cs +++ b/src/Elastic.Documentation.Configuration/Changelog/YamlLenientListConverter.cs @@ -6,7 +6,7 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace Elastic.Changelog.Serialization; +namespace Elastic.Documentation.Configuration.Changelog; /// /// YAML type converter for that accepts both string and list forms. diff --git a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj index 8796dc5fd7..ceb479fab7 100644 --- a/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj +++ b/src/Elastic.Documentation.Configuration/Elastic.Documentation.Configuration.csproj @@ -13,6 +13,7 @@ + @@ -20,7 +21,6 @@ - diff --git a/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs b/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs index 68f9b6ffd4..c7d176f59a 100644 --- a/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs +++ b/src/Elastic.Documentation.Configuration/Inference/VersionInference.cs @@ -6,6 +6,7 @@ using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Configuration.Inference; diff --git a/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs b/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs index 7956d58190..9dabf2d9ed 100644 --- a/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs +++ b/src/Elastic.Documentation.Configuration/ReleaseNotes/BundleLoader.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using System.Text.RegularExpressions; using Elastic.Documentation.ReleaseNotes; +using Elastic.Documentation.Versions; using YamlDotNet.Core; namespace Elastic.Documentation.Configuration.ReleaseNotes; diff --git a/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs b/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs deleted file mode 100644 index 9b10e71934..0000000000 --- a/src/Elastic.Documentation.Configuration/Serialization/LenientStringListConverter.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Elastic.Documentation.Configuration.Serialization; - -/// -/// YAML type converter for List<string> that accepts both comma-separated strings and YAML sequences. -/// Used by the minimal changelog DTO deserializer so that publish blocker fields like types and areas -/// can be specified as either "deprecation, known-issue" or a YAML list. -/// -public class LenientStringListConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(List); - - public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) - { - if (parser.TryConsume(out var scalar)) - { - if (string.IsNullOrEmpty(scalar.Value) || scalar.Value == "~") - return null; - - var items = scalar.Value - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .ToList(); - - return items.Count > 0 ? items : null; - } - - if (parser.TryConsume(out _)) - { - var items = new List(); - - while (!parser.TryConsume(out _)) - { - var item = parser.Consume(); - if (!string.IsNullOrWhiteSpace(item.Value)) - items.Add(item.Value.Trim()); - } - - return items.Count > 0 ? items : null; - } - - return null; - } - - public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) - { - if (value is not List { Count: > 0 } items) - { - emitter.Emit(new Scalar(null, null, string.Empty, ScalarStyle.Plain, true, false)); - return; - } - - emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block)); - - foreach (var item in items) - emitter.Emit(new Scalar(null, null, item, ScalarStyle.Plain, true, false)); - - emitter.Emit(new SequenceEnd()); - } -} diff --git a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs index 3bb5d61f93..21cab10863 100644 --- a/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs +++ b/src/Elastic.Documentation.Configuration/Serialization/YamlStaticContext.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Documentation.Configuration.Assembler; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.Codex; using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; @@ -61,4 +62,18 @@ namespace Elastic.Documentation.Configuration.Serialization; [YamlSerializable(typeof(ChangelogConfigMinimalDto))] [YamlSerializable(typeof(RulesConfigMinimalDto))] [YamlSerializable(typeof(PublishRulesMinimalDto))] +// Changelog YAML DTOs for CLI configuration (changelog.yml) +[YamlSerializable(typeof(ChangelogConfigurationYaml))] +[YamlSerializable(typeof(PivotConfigurationYaml))] +[YamlSerializable(typeof(TypeEntryYaml))] +[YamlSerializable(typeof(RulesConfigurationYaml))] +[YamlSerializable(typeof(CreateRulesYaml))] +[YamlSerializable(typeof(BundleRulesYaml))] +[YamlSerializable(typeof(PublishRulesYaml))] +[YamlSerializable(typeof(ProductsConfigYaml))] +[YamlSerializable(typeof(DefaultProductYaml))] +[YamlSerializable(typeof(BundleConfigurationYaml))] +[YamlSerializable(typeof(BundleProfileYaml))] +[YamlSerializable(typeof(ExtractConfigurationYaml))] +[YamlSerializable(typeof(YamlLenientList))] public partial class YamlStaticContext; diff --git a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs index b8e2d010d9..c9a7228981 100644 --- a/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs +++ b/src/Elastic.Documentation.Configuration/SystemEnvironmentVariables.cs @@ -2,6 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation; + namespace Elastic.Documentation.Configuration; /// diff --git a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs index 78e024151a..7e06c6c24d 100644 --- a/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs +++ b/src/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cs @@ -10,7 +10,7 @@ using Elastic.Documentation.Extensions; using Nullean.ScopedFileSystem; using YamlDotNet.Serialization; -using static Elastic.Documentation.Configuration.SymlinkValidator; +using static Elastic.Documentation.SymlinkValidator; namespace Elastic.Documentation.Configuration.Toc; diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs index f3ab2edf5d..48af422a4e 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.ComponentModel.DataAnnotations; +using Elastic.Documentation.Versions; using NetEscapades.EnumGenerators; using YamlDotNet.Serialization; diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs index 290b88c752..3458a686ea 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionsConfigurationExtensions.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation.Versions; using YamlDotNet.Serialization; namespace Elastic.Documentation.Configuration.Versions; diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs index 2c54f1d374..d4dc8d5cca 100644 --- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs +++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkFetcher.cs @@ -7,6 +7,7 @@ using System.IO.Abstractions; using System.Text.Json; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.LinkIndex; using Elastic.Documentation.Serialization; using Microsoft.Extensions.Logging; diff --git a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs index abaf584c2b..d329393b40 100644 --- a/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs +++ b/src/Elastic.Documentation.Links/CrossLinks/CrossLinkResolver.cs @@ -5,6 +5,7 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; namespace Elastic.Documentation.Links.CrossLinks; diff --git a/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs b/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs index 8e65d8137f..fc1ae6e2aa 100644 --- a/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs +++ b/src/Elastic.Documentation.Links/InboundLinks/LinkIndexLinkChecker.cs @@ -38,7 +38,7 @@ public async Task CheckRepository(IDiagnosticsCollector collector, string? var root = fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); if (fromRepository == null && toRepository == null) { - fromRepository ??= GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName; + fromRepository ??= GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName; if (fromRepository == null) throw new Exception("Unable to determine repository name"); } @@ -58,7 +58,7 @@ public async Task CheckWithLocalLinksJson(IDiagnosticsCollector collector, { file ??= ".artifacts/docs/html/links.json"; var root = !string.IsNullOrEmpty(path) ? fileSystem.DirectoryInfo.New(path) : fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); - var repository = GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName + var repository = GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName ?? throw new Exception("Unable to determine repository name"); var localLinksJson = fileSystem.FileInfo.New(Path.Join(root.FullName, file)); diff --git a/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj b/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj index 4bf0ef0294..76a05b7011 100644 --- a/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj +++ b/src/Elastic.Documentation.ServiceDefaults/Elastic.Documentation.ServiceDefaults.csproj @@ -17,6 +17,7 @@ + diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidLogProcessor.cs b/src/Elastic.Documentation.ServiceDefaults/Logging/EuidLogProcessor.cs similarity index 92% rename from src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidLogProcessor.cs rename to src/Elastic.Documentation.ServiceDefaults/Logging/EuidLogProcessor.cs index 3d1e783184..511fe2677e 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidLogProcessor.cs +++ b/src/Elastic.Documentation.ServiceDefaults/Logging/EuidLogProcessor.cs @@ -3,11 +3,11 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using Elastic.Documentation.Api.Core; +using Elastic.Documentation.ServiceDefaults.Telemetry; using OpenTelemetry; using OpenTelemetry.Logs; -namespace Elastic.Documentation.Api.Infrastructure.OpenTelemetry; +namespace Elastic.Documentation.ServiceDefaults.Logging; /// /// OpenTelemetry log processor that automatically adds user.euid attribute to log records diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs similarity index 51% rename from src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs rename to src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs index 41577a8e90..903ac7a416 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/OpenTelemetryExtensions.cs +++ b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidEnrichmentExtensions.cs @@ -3,8 +3,9 @@ // See the LICENSE file in the project root for more information using System.Reflection; -using Elastic.Documentation.Api.Core; +using Elastic.Documentation.ServiceDefaults.Logging; using Elastic.OpenTelemetry; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using OpenTelemetry; using OpenTelemetry.Logs; @@ -12,68 +13,18 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; -namespace Elastic.Documentation.Api.Infrastructure.OpenTelemetry; +namespace Elastic.Documentation.ServiceDefaults.Telemetry; -public static class OpenTelemetryExtensions +public static class EuidEnrichmentExtensions { /// - /// Configures logging for the Docs API with euid enrichment. - /// This is the shared configuration used in both production and tests. + /// Configures Elastic OpenTelemetry (EDOT) with euid enrichment for logging, tracing, and metrics. + /// Reads service.version from the calling assembly and configures the resource attribute. + /// No-ops if OTEL_EXPORTER_OTLP_ENDPOINT is not set. /// - public static LoggerProviderBuilder AddDocsApiLogging(this LoggerProviderBuilder builder) - { - _ = builder.AddProcessor(); - return builder; - } - - /// - /// Configures tracing for the Docs API with sources, instrumentation, and enrichment. - /// This is the shared configuration used in both production and tests. - /// - public static TracerProviderBuilder AddDocsApiTracing(this TracerProviderBuilder builder) - { - _ = builder - .AddSource(TelemetryConstants.AskAiSourceName) - .AddSource(TelemetryConstants.StreamTransformerSourceName) - .AddSource(TelemetryConstants.OtlpProxySourceName) - .AddSource(TelemetryConstants.CacheSourceName) - .AddSource(TelemetryConstants.AskAiFeedbackSourceName) - .AddSource(TelemetryConstants.McpToolSourceName) - .AddAspNetCoreInstrumentation(aspNetCoreOptions => - { - // Don't trace root API endpoint (health check) - aspNetCoreOptions.Filter = (httpContext) => - { - var path = httpContext.Request.Path.Value ?? string.Empty; - // Exclude root API path: /docs/_api/v1 - return path != "/docs/_api/v1"; - }; - - // Enrich spans with custom attributes from HTTP context - aspNetCoreOptions.EnrichWithHttpRequest = (activity, httpRequest) => - { - // Add euid cookie value to span attributes and baggage - if (httpRequest.Cookies.TryGetValue("euid", out var euid) && !string.IsNullOrEmpty(euid)) - { - _ = activity.SetTag(TelemetryConstants.UserEuidAttributeName, euid); - // Add to baggage so it propagates to all child spans - _ = activity.AddBaggage(TelemetryConstants.UserEuidAttributeName, euid); - } - }; - }) - .AddProcessor() // Automatically add euid to all child spans - .AddHttpClientInstrumentation(); - - return builder; - } - - /// - /// Configures Elastic OpenTelemetry (EDOT) for the Docs API. - /// - /// The web application builder + /// The host application builder /// The builder for chaining - public static TBuilder AddDocsApiOpenTelemetry(this TBuilder builder) - where TBuilder : IHostApplicationBuilder + public static TBuilder AddEuidEnrichment(this TBuilder builder) where TBuilder : IHostApplicationBuilder { var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); if (!useOtlpExporter) @@ -89,8 +40,27 @@ public static TBuilder AddDocsApiOpenTelemetry(this TBuilder builder) _ = builder.AddElasticOpenTelemetry(options, edotBuilder => { _ = edotBuilder - .WithLogging(logging => logging.AddDocsApiLogging()) - .WithTracing(tracing => tracing.AddDocsApiTracing()) + .WithLogging(logging => logging.AddProcessor()) + .WithTracing(tracing => + { + _ = tracing + .AddAspNetCoreInstrumentation(aspNetCoreOptions => + { + // Enrich spans with custom attributes from HTTP context + aspNetCoreOptions.EnrichWithHttpRequest = (activity, httpRequest) => + { + // Add euid cookie value to span attributes and baggage + if (httpRequest.Cookies.TryGetValue("euid", out var euid) && !string.IsNullOrEmpty(euid)) + { + _ = activity.SetTag(TelemetryConstants.UserEuidAttributeName, euid); + // Add to baggage so it propagates to all child spans + _ = activity.AddBaggage(TelemetryConstants.UserEuidAttributeName, euid); + } + }; + }) + .AddProcessor() // Automatically add euid to all child spans + .AddHttpClientInstrumentation(); + }) .WithMetrics(metrics => { _ = metrics @@ -110,8 +80,7 @@ public static TBuilder AddDocsApiOpenTelemetry(this TBuilder builder) private static void ConfigureServiceVersionAttributes(TBuilder builder) where TBuilder : IHostApplicationBuilder { - - var informationalVersion = Assembly.GetExecutingAssembly() + var informationalVersion = (Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly()) .GetCustomAttribute()?.InformationalVersion; if (informationalVersion is null) diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidSpanProcessor.cs b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidSpanProcessor.cs similarity index 91% rename from src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidSpanProcessor.cs rename to src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidSpanProcessor.cs index 33de9ceeb2..f9b83e478e 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/OpenTelemetry/EuidSpanProcessor.cs +++ b/src/Elastic.Documentation.ServiceDefaults/Telemetry/EuidSpanProcessor.cs @@ -3,10 +3,9 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using Elastic.Documentation.Api.Core; using OpenTelemetry; -namespace Elastic.Documentation.Api.Infrastructure.OpenTelemetry; +namespace Elastic.Documentation.ServiceDefaults.Telemetry; /// /// OpenTelemetry span processor that automatically adds user.euid tag to all spans diff --git a/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs b/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs index e4a90b864b..706cbb08d6 100644 --- a/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs +++ b/src/Elastic.Documentation.ServiceDefaults/Telemetry/TelemetryConstants.cs @@ -11,4 +11,9 @@ namespace Elastic.Documentation.ServiceDefaults.Telemetry; public static class TelemetryConstants { public const string AssemblerSyncInstrumentationName = "Elastic.Documentation.Assembler.Sync"; + + /// + /// Tag/baggage name used to annotate spans and log records with the user's EUID value. + /// + public const string UserEuidAttributeName = "user.euid"; } diff --git a/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj b/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj new file mode 100644 index 0000000000..1ab221484e --- /dev/null +++ b/src/Elastic.Documentation.Tooling/Elastic.Documentation.Tooling.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + Elastic.Documentation + true + true + $(NoWarn);CS1591;CS1573;CS1572;CS1571;CS1570;CS1574 + + + + + + + + + + + + + + + + + diff --git a/src/Elastic.Documentation/Exporter.cs b/src/Elastic.Documentation.Tooling/Exporter.cs similarity index 56% rename from src/Elastic.Documentation/Exporter.cs rename to src/Elastic.Documentation.Tooling/Exporter.cs index 25adbdf2df..6c45c60551 100644 --- a/src/Elastic.Documentation/Exporter.cs +++ b/src/Elastic.Documentation.Tooling/Exporter.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using Nullean.Argh; -using static Elastic.Documentation.Exporter; namespace Elastic.Documentation; @@ -21,6 +20,6 @@ public enum Exporter public static class ExportOptions { - public static HashSet Default { get; } = [Html, LLMText, Configuration, DocumentationState, LinkMetadata, Redirects]; - public static HashSet MetadataOnly { get; } = [Configuration, DocumentationState, LinkMetadata, Redirects]; + public static HashSet Default { get; } = [Exporter.Html, Exporter.LLMText, Exporter.Configuration, Exporter.DocumentationState, Exporter.LinkMetadata, Exporter.Redirects]; + public static HashSet MetadataOnly { get; } = [Exporter.Configuration, Exporter.DocumentationState, Exporter.LinkMetadata, Exporter.Redirects]; } diff --git a/src/Elastic.Documentation/ExternalCommands/ExternalCommandExecutor.cs b/src/Elastic.Documentation.Tooling/ExternalCommands/ExternalCommandExecutor.cs similarity index 100% rename from src/Elastic.Documentation/ExternalCommands/ExternalCommandExecutor.cs rename to src/Elastic.Documentation.Tooling/ExternalCommands/ExternalCommandExecutor.cs diff --git a/src/Elastic.Documentation/ExternalCommands/NoopConsoleWriter.cs b/src/Elastic.Documentation.Tooling/ExternalCommands/NoopConsoleWriter.cs similarity index 100% rename from src/Elastic.Documentation/ExternalCommands/NoopConsoleWriter.cs rename to src/Elastic.Documentation.Tooling/ExternalCommands/NoopConsoleWriter.cs diff --git a/src/Elastic.Documentation.Configuration/FileSystemFactory.cs b/src/Elastic.Documentation.Tooling/FileSystemFactory.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/FileSystemFactory.cs rename to src/Elastic.Documentation.Tooling/FileSystemFactory.cs index 9f8b55b4cc..09cb7411ee 100644 --- a/src/Elastic.Documentation.Configuration/FileSystemFactory.cs +++ b/src/Elastic.Documentation.Tooling/FileSystemFactory.cs @@ -6,6 +6,8 @@ using System.IO.Abstractions.TestingHelpers; using Nullean.ScopedFileSystem; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.Configuration; public static class FileSystemFactory diff --git a/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs new file mode 100644 index 0000000000..db58b514b3 --- /dev/null +++ b/src/Elastic.Documentation.Tooling/GitCheckoutInformationFactory.cs @@ -0,0 +1,149 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions; +using System.Text.RegularExpressions; +using Elastic.Documentation.Extensions; +using Microsoft.Extensions.Logging; +using Nullean.ScopedFileSystem; +using SoftCircuits.IniFileParser; + +namespace Elastic.Documentation; + +public static partial class GitCheckoutInformationFactory +{ + // manual read because libgit2sharp is not yet AOT ready + public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem fileSystem, ILogger? logger = null) + { + if (source is null) + return GitCheckoutInformation.Unavailable; + + // Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType + // (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper + // rather than relying on the outer type name. + var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType(); + if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)) + { + return new GitCheckoutInformation + { + Branch = $"test-e35fcb27-5f60-4e", + Remote = "elastic/docs-builder", + Ref = "e35fcb27-5f60-4e", + RepositoryName = "docs-builder" + }; + } + var fakeRef = Guid.NewGuid().ToString()[..16]; + + var gitDir = GitDir(source, ".git"); + if (!gitDir.Exists) + { + var worktreeFile = Git(source, ".git"); + if (!worktreeFile.Exists) + return GitCheckoutInformation.Unavailable; + var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty); + if (workTreePath is null) + return GitCheckoutInformation.Unavailable; + gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git"); + if (gitDir is null || !gitDir.Exists) + return GitCheckoutInformation.Unavailable; + } + + var gitConfig = Git(gitDir, "config"); + if (!gitConfig.Exists) + { + logger?.LogInformation("Git checkout information not available."); + return GitCheckoutInformation.Unavailable; + } + + var head = Read(gitDir, "HEAD") ?? fakeRef; + var gitRef = head; + var branch = head.Replace("refs/heads/", string.Empty); + if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase)) + { + head = head.Replace("ref: ", string.Empty); + gitRef = Read(gitDir, head) ?? fakeRef; + branch = branch.Replace("ref: ", string.Empty); + } + else + branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME") ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME") ?? "detached/head"; + + var ini = new IniFile(); + using var stream = gitConfig.OpenRead(); + using var streamReader = new StreamReader(stream); + ini.Load(streamReader); + + var remote = BranchTrackingRemote(branch, ini); + logger?.LogInformation("Remote from branch: {GitRemote}", remote); + if (string.IsNullOrEmpty(remote)) + { + remote = BranchTrackingRemote("main", ini); + logger?.LogInformation("Remote from main branch: {GitRemote}", remote); + } + + if (string.IsNullOrEmpty(remote)) + { + remote = BranchTrackingRemote("master", ini); + logger?.LogInformation("Remote from master branch: {GitRemote}", remote); + } + + if (string.IsNullOrEmpty(remote)) + { + remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY"); + logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote); + } + + if (string.IsNullOrEmpty(remote)) + { + remote = "elastic/docs-builder-unknown"; + logger?.LogInformation("Remote from fallback: {GitRemote}", remote); + } + remote = CutOffGitExtension().Replace(remote, string.Empty); + + var githubRef = Environment.GetEnvironmentVariable("GITHUB_REF"); + var info = new GitCheckoutInformation + { + Ref = gitRef, + Branch = branch, + Remote = remote, + RepositoryName = remote.Split('/').Last(), + GitHubRef = string.IsNullOrEmpty(githubRef) ? null : githubRef + }; + + logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote); + logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName); + return info; + + IFileInfo Git(IDirectoryInfo directoryInfo, string path) => + fileSystem.FileInfo.New(Path.Join(directoryInfo.FullName, path)); + + IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) => + fileSystem.DirectoryInfo.New(Path.Join(directoryInfo.FullName, path)); + + string? Read(IDirectoryInfo directoryInfo, string path) + { + var gitPath = Git(directoryInfo, path).FullName; + return !fileSystem.File.Exists(gitPath) + ? null + : fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray()); + } + + string BranchTrackingRemote(string b, IniFile c) + { + var sections = c.GetSections(); + var branchSection = $"branch \"{b}\""; + if (!sections.Contains(branchSection)) + return string.Empty; + + var remoteName = ini.GetSetting(branchSection, "remote")?.Trim(); + + var remoteSection = $"remote \"{remoteName}\""; + + remote = ini.GetSetting(remoteSection, "url"); + return remote ?? string.Empty; + } + } + + [GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase)] + private static partial Regex CutOffGitExtension(); +} diff --git a/src/Elastic.Documentation/GlobalCliOptions.cs b/src/Elastic.Documentation.Tooling/GlobalCliOptions.cs similarity index 84% rename from src/Elastic.Documentation/GlobalCliOptions.cs rename to src/Elastic.Documentation.Tooling/GlobalCliOptions.cs index cc0af96f8b..c6c203b8b8 100644 --- a/src/Elastic.Documentation/GlobalCliOptions.cs +++ b/src/Elastic.Documentation.Tooling/GlobalCliOptions.cs @@ -4,6 +4,8 @@ using Microsoft.Extensions.Logging; +// ReSharper disable once CheckNamespace — intentionally preserving the namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation; /// diff --git a/src/Elastic.Documentation.Configuration/Paths.cs b/src/Elastic.Documentation.Tooling/Paths.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/Paths.cs rename to src/Elastic.Documentation.Tooling/Paths.cs index fa24bef977..a3d7011301 100644 --- a/src/Elastic.Documentation.Configuration/Paths.cs +++ b/src/Elastic.Documentation.Tooling/Paths.cs @@ -6,6 +6,8 @@ using System.IO.Abstractions; using Elastic.Documentation.Extensions; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.Configuration; public static class Paths diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index e65d174ba5..10eb1c4b74 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -6,11 +6,10 @@ using System.Diagnostics.CodeAnalysis; using System.Text; using Elastic.Documentation.Diagnostics; -using YamlDotNet.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.AppliesTo; -[YamlSerializable] public record AppliesCollection : IReadOnlyCollection { private readonly Applicability[] _items; @@ -169,7 +168,6 @@ public override string ToString() public int Count => _items.Length; } -[YamlSerializable] public record Applicability : IComparable, IComparable { public ProductLifecycle Lifecycle { get; init; } diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index eb4a0f7f36..aa70d28259 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -2,6 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation.Versions; + namespace Elastic.Documentation.AppliesTo; /// diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs index 5bee5c81f2..c5a971b9cb 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs @@ -3,10 +3,12 @@ // See the LICENSE file in the project root for more information using System.Collections; +using System.Reflection; using System.Text; using System.Text.Json.Serialization; using Elastic.Documentation.Diagnostics; -using YamlDotNet.Serialization; +using Elastic.Documentation.Search; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.AppliesTo; @@ -34,26 +36,20 @@ public interface IApplicableToElement ApplicableTo? AppliesTo { get; } } -[YamlSerializable] [JsonConverter(typeof(ApplicableToJsonConverter))] public record ApplicableTo { - [YamlMember(Alias = "stack")] public AppliesCollection? Stack { get; set; } - [YamlMember(Alias = "deployment")] public DeploymentApplicability? Deployment { get; set; } - [YamlMember(Alias = "serverless")] public ServerlessProjectApplicability? Serverless { get; set; } - [YamlMember(Alias = "product")] public AppliesCollection? Product { get; set; } public ProductApplicability? ProductApplicability { get; set; } [JsonIgnore] - [YamlIgnore] public ApplicabilityDiagnosticsCollection? Diagnostics { get; set; } public static ApplicableTo All { get; } = new() @@ -72,6 +68,83 @@ public record ApplicableTo Serverless = ServerlessProjectApplicability.All }; + /// + /// Convert this rich applicability description into the flat wire-format entries indexed in Elasticsearch + /// under applies_to. Produces the same JSON shape as ApplicableToJsonConverter. + /// + public IReadOnlyCollection ToAppliesTo() + { + var entries = new List(); + + if (Stack is not null) + AddEntries(entries, "stack", "stack", Stack); + + if (Deployment is not null) + { + if (Deployment.Self is not null) + AddEntries(entries, "deployment", "self", Deployment.Self); + if (Deployment.Ece is not null) + AddEntries(entries, "deployment", "ece", Deployment.Ece); + if (Deployment.Eck is not null) + AddEntries(entries, "deployment", "eck", Deployment.Eck); + if (Deployment.Ess is not null) + AddEntries(entries, "deployment", "ess", Deployment.Ess); + } + + if (Serverless is not null) + { + if (Serverless.Elasticsearch is not null) + AddEntries(entries, "serverless", "elasticsearch", Serverless.Elasticsearch); + if (Serverless.Observability is not null) + AddEntries(entries, "serverless", "observability", Serverless.Observability); + if (Serverless.Security is not null) + AddEntries(entries, "serverless", "security", Serverless.Security); + } + + if (Product is not null) + AddEntries(entries, "product", "product", Product); + + if (ProductApplicability is not null) + { + foreach (var prop in typeof(ProductApplicability).GetProperties()) + { + var name = prop.GetCustomAttribute()?.Name; + if (name is null) + continue; + if (prop.GetValue(ProductApplicability) is AppliesCollection coll) + AddEntries(entries, "product", name, coll); + } + } + + return entries; + } + + private static void AddEntries(List sink, string type, string subType, AppliesCollection coll) + { + foreach (var a in coll) + sink.Add(new AppliesToEntry + { + Type = type, + SubType = subType, + Lifecycle = LifecycleName(a.Lifecycle), + Version = a.Version?.ToString() + }); + } + + private static string LifecycleName(ProductLifecycle lc) => lc switch + { + ProductLifecycle.TechnicalPreview => "preview", + ProductLifecycle.Beta => "beta", + ProductLifecycle.GenerallyAvailable => "ga", + ProductLifecycle.Deprecated => "deprecated", + ProductLifecycle.Removed => "removed", + ProductLifecycle.Unavailable => "unavailable", + ProductLifecycle.Development => "development", + ProductLifecycle.Planned => "planned", + ProductLifecycle.Discontinued => "discontinued", + _ => "ga" + }; + /// public override string ToString() { @@ -119,19 +192,14 @@ public override string ToString() } } -[YamlSerializable] public record DeploymentApplicability { - [YamlMember(Alias = "self")] public AppliesCollection? Self { get; set; } - [YamlMember(Alias = "ece")] public AppliesCollection? Ece { get; set; } - [YamlMember(Alias = "eck")] public AppliesCollection? Eck { get; set; } - [YamlMember(Alias = "ess")] public AppliesCollection? Ess { get; set; } public static DeploymentApplicability All { get; } = new() @@ -181,16 +249,12 @@ public override string ToString() } } -[YamlSerializable] public record ServerlessProjectApplicability { - [YamlMember(Alias = "elasticsearch")] public AppliesCollection? Elasticsearch { get; set; } - [YamlMember(Alias = "observability")] public AppliesCollection? Observability { get; set; } - [YamlMember(Alias = "security")] public AppliesCollection? Security { get; set; } /// diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs index a6dba46c4d..acafa95a4e 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs @@ -5,7 +5,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -using YamlDotNet.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.AppliesTo; @@ -154,9 +154,8 @@ public class ApplicableToJsonConverter : JsonConverter foreach (var (key, items) in productProps) { - // Find the property by YamlMember alias var property = productType.GetProperties() - .FirstOrDefault(p => p.GetCustomAttribute()?.Alias == key); + .FirstOrDefault(p => p.GetCustomAttribute()?.Name == key); property?.SetValue(productApplicability, new AppliesCollection(items.ToArray())); } @@ -209,11 +208,11 @@ public override void Write(Utf8JsonWriter writer, ApplicableTo value, JsonSerial var productType = typeof(ProductApplicability); foreach (var property in productType.GetProperties()) { - var yamlAlias = property.GetCustomAttribute()?.Alias; - if (yamlAlias != null) + var jsonName = property.GetCustomAttribute()?.Name; + if (jsonName != null) { if (property.GetValue(value.ProductApplicability) is AppliesCollection propertyValue) - WriteApplicabilityEntries(writer, "product", yamlAlias, propertyValue); + WriteApplicabilityEntries(writer, "product", jsonName, propertyValue); } } } diff --git a/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs b/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs index c486bcf47a..2c0ce7bb61 100644 --- a/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs +++ b/src/Elastic.Documentation/AppliesTo/ProductApplicability.cs @@ -3,83 +3,82 @@ // See the LICENSE file in the project root for more information using System.Text; -using YamlDotNet.Serialization; +using System.Text.Json.Serialization; namespace Elastic.Documentation.AppliesTo; -[YamlSerializable] public record ProductApplicability { - [YamlMember(Alias = "ecctl")] + [JsonPropertyName("ecctl")] public AppliesCollection? Ecctl { get; set; } - [YamlMember(Alias = "curator")] + [JsonPropertyName("curator")] public AppliesCollection? Curator { get; set; } - [YamlMember(Alias = "apm-agent-android")] + [JsonPropertyName("apm-agent-android")] public AppliesCollection? ApmAgentAndroid { get; set; } - [YamlMember(Alias = "apm-agent-dotnet")] + [JsonPropertyName("apm-agent-dotnet")] public AppliesCollection? ApmAgentDotnet { get; set; } - [YamlMember(Alias = "apm-agent-go")] + [JsonPropertyName("apm-agent-go")] public AppliesCollection? ApmAgentGo { get; set; } - [YamlMember(Alias = "apm-agent-ios")] + [JsonPropertyName("apm-agent-ios")] public AppliesCollection? ApmAgentIos { get; set; } - [YamlMember(Alias = "apm-agent-java")] + [JsonPropertyName("apm-agent-java")] public AppliesCollection? ApmAgentJava { get; set; } - [YamlMember(Alias = "apm-agent-node")] + [JsonPropertyName("apm-agent-node")] public AppliesCollection? ApmAgentNode { get; set; } - [YamlMember(Alias = "apm-agent-php")] + [JsonPropertyName("apm-agent-php")] public AppliesCollection? ApmAgentPhp { get; set; } - [YamlMember(Alias = "apm-agent-python")] + [JsonPropertyName("apm-agent-python")] public AppliesCollection? ApmAgentPython { get; set; } - [YamlMember(Alias = "apm-agent-ruby")] + [JsonPropertyName("apm-agent-ruby")] public AppliesCollection? ApmAgentRuby { get; set; } - [YamlMember(Alias = "apm-agent-rum-js")] + [JsonPropertyName("apm-agent-rum-js")] public AppliesCollection? ApmAgentRumJs { get; set; } - [YamlMember(Alias = "edot-ios")] + [JsonPropertyName("edot-ios")] public AppliesCollection? EdotIos { get; set; } - [YamlMember(Alias = "edot-android")] + [JsonPropertyName("edot-android")] public AppliesCollection? EdotAndroid { get; set; } - [YamlMember(Alias = "edot-dotnet")] + [JsonPropertyName("edot-dotnet")] public AppliesCollection? EdotDotnet { get; set; } - [YamlMember(Alias = "edot-java")] + [JsonPropertyName("edot-java")] public AppliesCollection? EdotJava { get; set; } - [YamlMember(Alias = "edot-node")] + [JsonPropertyName("edot-node")] public AppliesCollection? EdotNode { get; set; } - [YamlMember(Alias = "edot-browser")] + [JsonPropertyName("edot-browser")] public AppliesCollection? EdotBrowser { get; set; } - [YamlMember(Alias = "edot-php")] + [JsonPropertyName("edot-php")] public AppliesCollection? EdotPhp { get; set; } - [YamlMember(Alias = "edot-python")] + [JsonPropertyName("edot-python")] public AppliesCollection? EdotPython { get; set; } - [YamlMember(Alias = "edot-cf-aws")] + [JsonPropertyName("edot-cf-aws")] public AppliesCollection? EdotCfAws { get; set; } - [YamlMember(Alias = "edot-cf-azure")] + [JsonPropertyName("edot-cf-azure")] public AppliesCollection? EdotCfAzure { get; set; } - [YamlMember(Alias = "edot-cf-gcp")] + [JsonPropertyName("edot-cf-gcp")] public AppliesCollection? EdotCfGcp { get; set; } - [YamlMember(Alias = "edot-collector")] + [JsonPropertyName("edot-collector")] public AppliesCollection? EdotCollector { get; set; } /// diff --git a/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs b/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs index 5f190b8304..c678c557d8 100644 --- a/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs +++ b/src/Elastic.Documentation/AppliesTo/ProductLifecycle.cs @@ -2,40 +2,39 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using YamlDotNet.Serialization; +using System.Text.Json.Serialization; namespace Elastic.Documentation.AppliesTo; -[YamlSerializable] public enum ProductLifecycle { // technical preview (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#beta-dev-and-preview-experimental) - [YamlMember(Alias = "preview")] + [JsonStringEnumMemberName("preview")] TechnicalPreview, // beta (ditto) - [YamlMember(Alias = "beta")] + [JsonStringEnumMemberName("beta")] Beta, // ga (replaces "added" in the current docs system since it was not entirely clear how/if that overlapped with beta/preview states) - [YamlMember(Alias = "ga")] + [JsonStringEnumMemberName("ga")] GenerallyAvailable, // deprecated (exists in current docs system per https://github.com/elastic/docs?tab=readme-ov-file#additions-and-deprecations) - [YamlMember(Alias = "deprecated")] + [JsonStringEnumMemberName("deprecated")] Deprecated, // removed content - [YamlMember(Alias = "removed")] + [JsonStringEnumMemberName("removed")] Removed, // unavailable (for content that doesn't exist in a specific context and is never coming or not coming anytime soon) - [YamlMember(Alias = "unavailable")] + [JsonStringEnumMemberName("unavailable")] Unavailable, // TODO remove these enum members in a future version when docs have been cleaned up // discontinued (historically we've immediately removed content when the feature ceases to be supported, but this might not be the case with pages that contain information that spans versions) - [YamlMember(Alias = "discontinued")] + [JsonStringEnumMemberName("discontinued")] Discontinued, // coming (ditto) - [YamlMember(Alias = "planned")] + [JsonStringEnumMemberName("planned")] Planned, // dev (ditto, though it's uncertain whether it's ever used or still needed) - [YamlMember(Alias = "development")] + [JsonStringEnumMemberName("development")] Development, } diff --git a/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs b/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs index ec3fc38f32..156fc94e6f 100644 --- a/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs +++ b/src/Elastic.Documentation/Diagnostics/DiagnosticsCollector.cs @@ -5,7 +5,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.IO.Abstractions; -using Microsoft.Extensions.Hosting; namespace Elastic.Documentation.Diagnostics; @@ -33,11 +32,13 @@ public class DiagnosticsCollector(IReadOnlyCollection output public virtual DiagnosticsCollector StartAsync(Cancel ctx) { - _ = ((IHostedService)this).StartAsync(ctx); + _ = EnsureStarted(ctx); return this; } - Task IHostedService.StartAsync(Cancel cancellationToken) + Task IDiagnosticsCollector.StartAsync(Cancel cancellationToken) => EnsureStarted(cancellationToken); + + private Task EnsureStarted(Cancel cancellationToken) { if (_started is not null) return _started; diff --git a/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs b/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs index 6e804ad76e..7a8203fa93 100644 --- a/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs +++ b/src/Elastic.Documentation/Diagnostics/IDiagnosticsCollector.cs @@ -4,11 +4,10 @@ using System.Collections.Concurrent; using System.IO.Abstractions; -using Microsoft.Extensions.Hosting; namespace Elastic.Documentation.Diagnostics; -public interface IDiagnosticsCollector : IAsyncDisposable, IHostedService +public interface IDiagnosticsCollector : IAsyncDisposable { int Warnings { get; } int Errors { get; } @@ -21,6 +20,9 @@ public interface IDiagnosticsCollector : IAsyncDisposable, IHostedService HashSet OffendingFiles { get; } ConcurrentDictionary InUseSubstitutionKeys { get; } + Task StartAsync(Cancel cancellationToken); + Task StopAsync(Cancel cancellationToken); + void Emit(Severity severity, string file, string message); void EmitError(string file, string message, Exception? e = null); void EmitError(string file, string message, string specificErrorMessage); diff --git a/src/Elastic.Documentation/Elastic.Documentation.csproj b/src/Elastic.Documentation/Elastic.Documentation.csproj index 3fe487e15c..cf4182443a 100644 --- a/src/Elastic.Documentation/Elastic.Documentation.csproj +++ b/src/Elastic.Documentation/Elastic.Documentation.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -11,18 +11,13 @@ - - - + + + + - - - - - - diff --git a/src/Elastic.Documentation/GitCheckoutInformation.cs b/src/Elastic.Documentation/GitCheckoutInformation.cs index a0307b267e..d1714428cd 100644 --- a/src/Elastic.Documentation/GitCheckoutInformation.cs +++ b/src/Elastic.Documentation/GitCheckoutInformation.cs @@ -2,17 +2,11 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.IO.Abstractions; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using Elastic.Documentation.Extensions; -using Microsoft.Extensions.Logging; -using Nullean.ScopedFileSystem; -using SoftCircuits.IniFileParser; namespace Elastic.Documentation; -public partial record GitCheckoutInformation +public record GitCheckoutInformation { public static GitCheckoutInformation Unavailable { get; } = new() { @@ -68,11 +62,9 @@ public partial record GitCheckoutInformation if (path is null) return null; - // Strip trailing .git if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase)) path = path[..^4]; - // Validate: must be exactly org/repo — two non-empty segments, no extra slashes var parts = path.Split('/'); if (parts.Length != 2 || string.IsNullOrEmpty(parts[0]) || string.IsNullOrEmpty(parts[1])) return null; @@ -85,161 +77,20 @@ public partial record GitCheckoutInformation { const string githubHost = "github.com"; - // git@github.com:org/repo.git → org/repo.git if (remote.StartsWith("git@github.com:", StringComparison.OrdinalIgnoreCase)) return remote["git@github.com:".Length..].TrimStart('/'); - // ssh://git@github.com/org/repo.git → org/repo.git if (remote.StartsWith("ssh://git@github.com/", StringComparison.OrdinalIgnoreCase)) return remote["ssh://git@github.com/".Length..].TrimStart('/'); - // https://github.com/org/repo.git or http://github.com/org/repo.git → org/repo.git if (remote.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase)) return remote["https://github.com/".Length..].TrimStart('/'); if (remote.StartsWith("http://github.com/", StringComparison.OrdinalIgnoreCase)) return remote["http://github.com/".Length..].TrimStart('/'); - // Bare org/repo (e.g. GITHUB_REPOSITORY env var) — must not contain "://" or "@" (i.e. not a URL) if (!remote.Contains("://") && !remote.Contains('@') && !remote.Contains(githubHost, StringComparison.OrdinalIgnoreCase)) return remote.TrimStart('/'); return null; } - - // manual read because libgit2sharp is not yet AOT ready - public static GitCheckoutInformation Create(IDirectoryInfo? source, IFileSystem fileSystem, ILogger? logger = null) - { - if (source is null) - return Unavailable; - - // Return test data for in-memory (mock) file systems. Use ScopedFileSystem.InnerType - // (available since Nullean.ScopedFileSystem 0.4.0) to inspect through the scope wrapper - // rather than relying on the outer type name. - var fsType = fileSystem is ScopedFileSystem sf ? sf.InnerType : fileSystem.GetType(); - if (fsType.Name.Contains("Mock", StringComparison.OrdinalIgnoreCase)) - { - return new GitCheckoutInformation - { - Branch = $"test-e35fcb27-5f60-4e", - Remote = "elastic/docs-builder", - Ref = "e35fcb27-5f60-4e", - RepositoryName = "docs-builder" - }; - } - var fakeRef = Guid.NewGuid().ToString()[..16]; - - var gitDir = GitDir(source, ".git"); - if (!gitDir.Exists) - { - // try a worktree .git file - var worktreeFile = Git(source, ".git"); - if (!worktreeFile.Exists) - return Unavailable; - var workTreePath = Read(source, ".git")?.Replace("gitdir: ", string.Empty); - if (workTreePath is null) - return Unavailable; - //TODO read branch info from worktree do not fall through - gitDir = fileSystem.DirectoryInfo.New(workTreePath).GetParent(".git"); - if (gitDir is null || !gitDir.Exists) - return Unavailable; - } - - var gitConfig = Git(gitDir, "config"); - if (!gitConfig.Exists) - { - logger?.LogInformation("Git checkout information not available."); - return Unavailable; - } - - var head = Read(gitDir, "HEAD") ?? fakeRef; - var gitRef = head; - var branch = head.Replace("refs/heads/", string.Empty); - //not detached HEAD - if (head.StartsWith("ref:", StringComparison.OrdinalIgnoreCase)) - { - head = head.Replace("ref: ", string.Empty); - gitRef = Read(gitDir, head) ?? fakeRef; - branch = branch.Replace("ref: ", string.Empty); - } - else - branch = Environment.GetEnvironmentVariable("GITHUB_PR_REF_NAME") ?? Environment.GetEnvironmentVariable("GITHUB_REF_NAME") ?? "detached/head"; - - var ini = new IniFile(); - using var stream = gitConfig.OpenRead(); - using var streamReader = new StreamReader(stream); - ini.Load(streamReader); - - var remote = BranchTrackingRemote(branch, ini); - logger?.LogInformation("Remote from branch: {GitRemote}", remote); - if (string.IsNullOrEmpty(remote)) - { - remote = BranchTrackingRemote("main", ini); - logger?.LogInformation("Remote from main branch: {GitRemote}", remote); - } - - if (string.IsNullOrEmpty(remote)) - { - remote = BranchTrackingRemote("master", ini); - logger?.LogInformation("Remote from master branch: {GitRemote}", remote); - } - - if (string.IsNullOrEmpty(remote)) - { - remote = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY"); - logger?.LogInformation("Remote from GITHUB_REPOSITORY: {GitRemote}", remote); - } - - if (string.IsNullOrEmpty(remote)) - { - remote = "elastic/docs-builder-unknown"; - logger?.LogInformation("Remote from fallback: {GitRemote}", remote); - } - remote = CutOffGitExtension().Replace(remote, string.Empty); - - var githubRef = Environment.GetEnvironmentVariable("GITHUB_REF"); - var info = new GitCheckoutInformation - { - Ref = gitRef, - Branch = branch, - Remote = remote, - RepositoryName = remote.Split('/').Last(), - GitHubRef = string.IsNullOrEmpty(githubRef) ? null : githubRef - }; - - logger?.LogInformation("-> Remote Name: {GitRemote}", info.Remote); - logger?.LogInformation("-> Repository Name: {RepositoryName}", info.RepositoryName); - return info; - - IFileInfo Git(IDirectoryInfo directoryInfo, string path) => - fileSystem.FileInfo.New(Path.Join(directoryInfo.FullName, path)); - - IDirectoryInfo GitDir(IDirectoryInfo directoryInfo, string path) => - fileSystem.DirectoryInfo.New(Path.Join(directoryInfo.FullName, path)); - - string? Read(IDirectoryInfo directoryInfo, string path) - { - var gitPath = Git(directoryInfo, path).FullName; - return !fileSystem.File.Exists(gitPath) - ? null - : fileSystem.File.ReadAllText(gitPath).Trim(Environment.NewLine.ToCharArray()); - } - - string BranchTrackingRemote(string b, IniFile c) - { - var sections = c.GetSections(); - var branchSection = $"branch \"{b}\""; - if (!sections.Contains(branchSection)) - return string.Empty; - - var remoteName = ini.GetSetting(branchSection, "remote")?.Trim(); - - var remoteSection = $"remote \"{remoteName}\""; - - remote = ini.GetSetting(remoteSection, "url"); - return remote ?? string.Empty; - } - } - - [GeneratedRegex(@"\.git$", RegexOptions.IgnoreCase)] - private static partial Regex CutOffGitExtension(); } diff --git a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs b/src/Elastic.Documentation/IEnvironmentVariables.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs rename to src/Elastic.Documentation/IEnvironmentVariables.cs index 1781abe626..b79944b8bc 100644 --- a/src/Elastic.Documentation.Configuration/IEnvironmentVariables.cs +++ b/src/Elastic.Documentation/IEnvironmentVariables.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation; /// /// Abstracts access to environment variables for testability. diff --git a/src/Elastic.Documentation/ChangelogEntrySubtype.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs similarity index 96% rename from src/Elastic.Documentation/ChangelogEntrySubtype.cs rename to src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs index dc51d8ce6f..f59d52833b 100644 --- a/src/Elastic.Documentation/ChangelogEntrySubtype.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntrySubtype.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation; +namespace Elastic.Documentation.ReleaseNotes; /// /// Enum representing changelog entry subtypes (only applicable to breaking changes) diff --git a/src/Elastic.Documentation/ChangelogEntryType.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs similarity index 97% rename from src/Elastic.Documentation/ChangelogEntryType.cs rename to src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs index 73c3bf9df6..32ce2ae362 100644 --- a/src/Elastic.Documentation/ChangelogEntryType.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntryType.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation; +namespace Elastic.Documentation.ReleaseNotes; /// /// Enum representing changelog entry types diff --git a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs index cde2ac16ee..40595dd689 100644 --- a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs +++ b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs @@ -7,6 +7,7 @@ using Elastic.Documentation.Links; using Elastic.Documentation.Search; using Elastic.Documentation.State; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Serialization; @@ -19,6 +20,7 @@ namespace Elastic.Documentation.Serialization; [JsonSerializable(typeof(LinkRegistry))] [JsonSerializable(typeof(LinkRegistryEntry))] [JsonSerializable(typeof(DocumentationDocument))] +[JsonSerializable(typeof(AppliesToEntry))] [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(ApplicableTo))] [JsonSerializable(typeof(AppliesCollection))] diff --git a/src/Elastic.Documentation.Configuration/SymlinkValidator.cs b/src/Elastic.Documentation/SymlinkValidator.cs similarity index 98% rename from src/Elastic.Documentation.Configuration/SymlinkValidator.cs rename to src/Elastic.Documentation/SymlinkValidator.cs index 0cbc6747ba..5612c577ab 100644 --- a/src/Elastic.Documentation.Configuration/SymlinkValidator.cs +++ b/src/Elastic.Documentation/SymlinkValidator.cs @@ -6,7 +6,7 @@ using System.Security; using Elastic.Documentation.Extensions; -namespace Elastic.Documentation.Configuration; +namespace Elastic.Documentation; /// /// Provides validation to ensure control files (docset.yml, toc.yml, redirects.yml) diff --git a/src/Elastic.Documentation/SemVersion.cs b/src/Elastic.Documentation/Versions/SemVersion.cs similarity index 99% rename from src/Elastic.Documentation/SemVersion.cs rename to src/Elastic.Documentation/Versions/SemVersion.cs index 6784bb4848..89e853f392 100644 --- a/src/Elastic.Documentation/SemVersion.cs +++ b/src/Elastic.Documentation/Versions/SemVersion.cs @@ -6,7 +6,7 @@ using System.Globalization; using System.Text.RegularExpressions; -namespace Elastic.Documentation; +namespace Elastic.Documentation.Versions; public class AllVersions() : SemVersion(99999, 0, 0) { @@ -307,4 +307,3 @@ private static int CompareComponent(string a, string b, bool lower = false) [GeneratedRegex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")] private static partial Regex MyRegex(); } - diff --git a/src/Elastic.Documentation/VersionOrDate.cs b/src/Elastic.Documentation/Versions/VersionOrDate.cs similarity index 99% rename from src/Elastic.Documentation/VersionOrDate.cs rename to src/Elastic.Documentation/Versions/VersionOrDate.cs index 7325e3c9d9..513c28c8d5 100644 --- a/src/Elastic.Documentation/VersionOrDate.cs +++ b/src/Elastic.Documentation/Versions/VersionOrDate.cs @@ -4,7 +4,7 @@ using System.Globalization; -namespace Elastic.Documentation; +namespace Elastic.Documentation.Versions; /// /// A version representation that can be either a semantic version or a date. diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/Versions/VersionSpec.cs similarity index 99% rename from src/Elastic.Documentation/VersionSpec.cs rename to src/Elastic.Documentation/Versions/VersionSpec.cs index 67881a2168..0fdf7f36ff 100644 --- a/src/Elastic.Documentation/VersionSpec.cs +++ b/src/Elastic.Documentation/Versions/VersionSpec.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; -namespace Elastic.Documentation; +namespace Elastic.Documentation.Versions; public sealed class AllVersionsSpec : VersionSpec { diff --git a/src/Elastic.Markdown/Elastic.Markdown.csproj b/src/Elastic.Markdown/Elastic.Markdown.csproj index ed5f7a3a37..64abeb65e1 100644 --- a/src/Elastic.Markdown/Elastic.Markdown.csproj +++ b/src/Elastic.Markdown/Elastic.Markdown.csproj @@ -38,6 +38,7 @@ + diff --git a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs index 1725c842a0..b44d144135 100644 --- a/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs +++ b/src/Elastic.Markdown/Exporters/Elasticsearch/ElasticsearchMarkdownExporter.Export.cs @@ -127,7 +127,7 @@ public async ValueTask ExportAsync(MarkdownExportFileContext fileContext, StrippedBody = strippedBody, Description = fileContext.SourceFile.YamlFrontMatter?.Description, Abstract = @abstract, - Applies = appliesTo, + Applies = appliesTo.ToAppliesTo(), Parents = navigation.GetParentsOfMarkdownFile(file).Select(i => new ParentDocument { Title = i.NavigationTitle, diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs similarity index 98% rename from src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs rename to src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs index bd1fd63222..d8523168a4 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Markdown/Myst/AppliesTo/ApplicableToYamlConverter.cs @@ -4,10 +4,13 @@ using System.Diagnostics.CodeAnalysis; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Versions; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.AppliesTo; public class ApplicableToYamlConverter(IReadOnlyCollection productKeys) : IYamlTypeConverter diff --git a/src/Elastic.Documentation/AppliesTo/AllVersions.cs b/src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs similarity index 87% rename from src/Elastic.Documentation/AppliesTo/AllVersions.cs rename to src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs index 39b9e7325a..aecb1a3d5d 100644 --- a/src/Elastic.Documentation/AppliesTo/AllVersions.cs +++ b/src/Elastic.Markdown/Myst/AppliesTo/SemVersionConverter.cs @@ -2,10 +2,13 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation.Versions; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; +// ReSharper disable once CheckNamespace — intentionally preserving the original namespace so consumers need no using changes +#pragma warning disable IDE0130 namespace Elastic.Documentation.AppliesTo; public class SemVersionConverter : IYamlTypeConverter diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index a6a3dd4a6d..dcfcee4639 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -6,6 +6,7 @@ using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.Components; diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index 82afa2ce5f..c7b858d396 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -5,6 +5,7 @@ using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.Components; diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs index 954c932d5a..ae1d2f67c4 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogBlock.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Extensions; using Elastic.Documentation.ReleaseNotes; +using Elastic.Documentation.Versions; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.Helpers; diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs index f58e3f9f30..114f402bfe 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs @@ -6,6 +6,7 @@ using System.Text; using Elastic.Documentation; using Elastic.Documentation.ReleaseNotes; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.Directives.Changelog; diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs index 56e2d10416..4d4d7fcc6d 100644 --- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Extensions; using Elastic.Markdown.Diagnostics; diff --git a/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs index 6f1a8efe41..3f5a4e81d5 100644 --- a/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Include/IncludeBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Extensions; using Elastic.Markdown.Diagnostics; diff --git a/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs b/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs index cff7043561..69ca93a98d 100644 --- a/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Settings/SettingsBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Extensions; using Elastic.Markdown.Diagnostics; diff --git a/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs b/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs index 87bab6f033..e01293cbd5 100644 --- a/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Version/VersionBlock.cs @@ -4,6 +4,7 @@ using System.Globalization; using Elastic.Documentation; +using Elastic.Documentation.Versions; using Elastic.Markdown.Diagnostics; using static System.StringSplitOptions; diff --git a/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs b/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs index f5e696a9c3..6383623d98 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/Substitution/SubstitutionMutationHelper.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Elastic.Documentation; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Myst.InlineParsers.Substitution; diff --git a/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs b/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs index d65d9dd06c..a2803228be 100644 --- a/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs +++ b/src/Elastic.Markdown/Myst/Roles/AppliesTo/AppliesToRole.cs @@ -6,6 +6,7 @@ using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Versions; using Elastic.Markdown.Diagnostics; using Markdig; using Markdig.Parsers; diff --git a/src/Elastic.Markdown/Myst/YamlSerialization.cs b/src/Elastic.Markdown/Myst/YamlSerialization.cs index 4f8502e75f..7e38e24f33 100644 --- a/src/Elastic.Markdown/Myst/YamlSerialization.cs +++ b/src/Elastic.Markdown/Myst/YamlSerialization.cs @@ -38,7 +38,6 @@ public static T Deserialize(string yaml, ProductsConfiguration products) [YamlSerializable(typeof(Setting))] [YamlSerializable(typeof(AllowedValue))] [YamlSerializable(typeof(SettingMutability))] -[YamlSerializable(typeof(ApplicableTo))] [YamlSerializable(typeof(ContributorEntry))] [YamlSerializable(typeof(ChangelogDirectiveConfigYaml))] [YamlSerializable(typeof(ChangelogDirectiveBundleConfigYaml))] diff --git a/src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj b/src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj deleted file mode 100644 index e9be25a83a..0000000000 --- a/src/api/Elastic.Documentation.Api.App/Elastic.Documentation.Api.App.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - Exe - net10.0 - enable - enable - true - - docs-builder-api - - true - true - true - true - false - Linux - - Elastic.Documentation.Api.App - - - - - - - - - - - - - - diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackUsecase.cs b/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackUsecase.cs deleted file mode 100644 index 82235e3924..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackUsecase.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Diagnostics; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Core.AskAi; - -/// -/// Use case for handling Ask AI message feedback submissions. -/// -public class AskAiMessageFeedbackUsecase( - IAskAiMessageFeedbackGateway feedbackGateway, - ILogger logger) -{ - private static readonly ActivitySource FeedbackActivitySource = new(TelemetryConstants.AskAiFeedbackSourceName); - - public async Task SubmitFeedback(AskAiMessageFeedbackRequest request, string? euid, CancellationToken ctx) - { - using var activity = FeedbackActivitySource.StartActivity("record message-feedback", ActivityKind.Internal); - _ = activity?.SetTag("gen_ai.conversation.id", request.ConversationId); // correlation with chat traces - _ = activity?.SetTag("ask_ai.message.id", request.MessageId); - _ = activity?.SetTag("ask_ai.feedback.reaction", request.Reaction.ToString().ToLowerInvariant()); - // Note: user.euid is automatically added to spans by EuidSpanProcessor - - // MessageId and ConversationId are Guid types, so no sanitization needed - logger.LogInformation( - "Recording message feedback for message {MessageId} in conversation {ConversationId}: {Reaction}", - request.MessageId, - request.ConversationId, - request.Reaction); - - var record = new AskAiMessageFeedbackRecord( - request.MessageId, - request.ConversationId, - request.Reaction, - euid - ); - - await feedbackGateway.RecordFeedbackAsync(record, ctx); - } -} diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs b/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs deleted file mode 100644 index 9c5c9f5b77..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiUsecase.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Diagnostics; -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Core.AskAi; - -public class AskAiUsecase( - IAskAiGateway askAiGateway, - IStreamTransformer streamTransformer, - ILogger logger) -{ - private static readonly ActivitySource AskAiActivitySource = new(TelemetryConstants.AskAiSourceName); - - public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx) - { - logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId); - var activity = AskAiActivitySource.StartActivity($"chat {streamTransformer.AgentProvider}", ActivityKind.Client); - _ = activity?.SetTag("gen_ai.operation.name", "chat"); - _ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider); // agent-builder or llm-gateway - _ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId); // docs-agent or docs_assistant - if (askAiRequest.ConversationId is not null) - _ = activity?.SetTag("gen_ai.conversation.id", askAiRequest.ConversationId.ToString()); - - var inputMessages = new[] - { - new InputMessage("user", [new MessagePart("text", askAiRequest.Message)]) - }; - var inputMessagesJson = JsonSerializer.Serialize(inputMessages, ApiJsonContext.Default.InputMessageArray); - _ = activity?.SetTag("gen_ai.input.messages", inputMessagesJson); - var sanitizedMessage = askAiRequest.Message?.Replace("\r", "").Replace("\n", ""); - logger.LogInformation("AskAI input message: <{ask_ai.input.message}>", sanitizedMessage); - logger.LogInformation("Streaming AskAI response"); - - // Gateway handles conversation ID generation if needed - var response = await askAiGateway.AskAi(askAiRequest, ctx); - - // Use generated ID if available, otherwise use the original request ID - var conversationId = response.GeneratedConversationId ?? askAiRequest.ConversationId; - if (conversationId is not null) - _ = activity?.SetTag("gen_ai.conversation.id", conversationId.ToString()); - - // The stream transformer takes ownership of the activity and disposes it when streaming completes. - // This is necessary because streaming happens asynchronously after this method returns. - var transformedStream = await streamTransformer.TransformAsync( - response.Stream, - response.GeneratedConversationId, - activity, - ctx); - return transformedStream; - } -} - -public record AskAiRequest(string Message, Guid? ConversationId); diff --git a/src/api/Elastic.Documentation.Api.Core/Changes/ChangesUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Changes/ChangesUsecase.cs deleted file mode 100644 index fac3706e64..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/Changes/ChangesUsecase.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Buffers; -using System.Text; -using System.Text.Json; -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Core.Changes; - -/// Use case for the documentation changes feed. -public partial class ChangesUsecase(IChangesGateway changesGateway, ILogger logger) -{ - public async Task GetChangesAsync(ChangesApiRequest request, Cancel ctx = default) - { - var cursor = DecodeCursor(request.Cursor); - var pageSize = Math.Clamp(request.PageSize, 1, ChangesDefaults.MaxPageSize); - - var result = await changesGateway.GetChangesAsync( - new ChangesRequest - { - Since = request.Since, - PageSize = pageSize, - Cursor = cursor - }, - ctx - ); - - var nextCursor = result.NextCursor is not null - ? EncodeCursor(result.NextCursor) - : null; - - var hasMore = nextCursor is not null; - - LogChanges(logger, request.Since, result.Pages.Count, hasMore); - - return new ChangesApiResponse - { - Pages = result.Pages, - HasMore = hasMore, - NextCursor = nextCursor - }; - } - - private static ChangesPageCursor? DecodeCursor(string? cursor) - { - if (string.IsNullOrWhiteSpace(cursor)) - return null; - - try - { - var remainder = cursor.Length % 4; - var paddingLength = (4 - remainder) % 4; - var base64 = cursor - .Replace('-', '+') - .Replace('_', '/') - + new string('=', paddingLength); - - var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - var arrayLength = root.GetArrayLength(); - if (root.ValueKind != JsonValueKind.Array || arrayLength < 2) - return null; - - var epochEl = root[0]; - var urlEl = root[1]; - if (epochEl.ValueKind != JsonValueKind.Number || urlEl.ValueKind != JsonValueKind.String) - return null; - - return new ChangesPageCursor(epochEl.GetInt64(), urlEl.GetString()!); - } - catch (Exception ex) when (ex is FormatException or JsonException or InvalidOperationException) - { - return null; - } - } - - private static string EncodeCursor(ChangesPageCursor cursor) - { - var buffer = new ArrayBufferWriter(); - using var writer = new Utf8JsonWriter(buffer); - writer.WriteStartArray(); - writer.WriteNumberValue(cursor.ContentLastUpdatedEpochMs); - writer.WriteStringValue(cursor.Url); - writer.WriteEndArray(); - writer.Flush(); - - return Convert.ToBase64String(buffer.WrittenSpan) - .TrimEnd('=') - .Replace('+', '-') - .Replace('/', '_'); - } - - [LoggerMessage(Level = LogLevel.Information, - Message = "Changes feed returned {Count} pages since {Since} (hasMore: {HasMore})")] - private static partial void LogChanges(ILogger logger, DateTimeOffset since, int count, bool hasMore); -} - -/// API request for the changes feed endpoint. -public record ChangesApiRequest -{ - public required DateTimeOffset Since { get; init; } - public int PageSize { get; init; } = ChangesDefaults.PageSize; - public string? Cursor { get; init; } -} - -/// API response for the changes feed endpoint. -public record ChangesApiResponse -{ - public required IReadOnlyList Pages { get; init; } - public required bool HasMore { get; init; } - public string? NextCursor { get; init; } -} - -/// A single changed page in the API response. -public record ChangedPageDto -{ - public required string Url { get; init; } - public required string Title { get; init; } - public required DateTimeOffset LastUpdated { get; init; } -} diff --git a/src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj b/src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj deleted file mode 100644 index 366861512a..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net10.0 - enable - enable - Elastic.Documentation.Api.Core - Elastic.Documentation.Api.Core - - - - - - - - - - diff --git a/src/api/Elastic.Documentation.Api.Core/Search/FullSearchUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Search/FullSearchUsecase.cs deleted file mode 100644 index 0f9d00c933..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/Search/FullSearchUsecase.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Core.Search; - -/// -/// Use case for full-page search operations. -/// -public partial class FullSearchUsecase(IFullSearchGateway fullSearchGateway, ILogger logger) -{ - public async Task SearchAsync(FullSearchApiRequest request, Cancel ctx = default) - { - var result = await fullSearchGateway.SearchAsync( - new FullSearchRequest - { - Query = request.Query, - PageNumber = request.PageNumber, - PageSize = request.PageSize, - TypeFilter = request.TypeFilter, - SectionFilter = request.SectionFilter, - DeploymentFilter = request.DeploymentFilter, - ProductFilter = request.ProductFilter, - VersionFilter = request.VersionFilter, - SortBy = request.SortBy - }, - ctx - ); - - var response = new FullSearchApiResponse - { - Results = result.Results, - TotalResults = result.TotalHits, - PageNumber = request.PageNumber, - PageSize = request.PageSize, - Aggregations = result.Aggregations, - IsSemanticQuery = result.IsSemanticQuery - }; - - LogFullSearchResults( - logger, - response.PageSize, - response.PageNumber, - request.Query, - result.IsSemanticQuery, - new FullSearchResultsLogProperties(result.Results.Select(i => i.Url).ToArray()) - ); - - return response; - } - - [LoggerMessage(Level = LogLevel.Information, Message = "Full search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}' (semantic: {IsSemantic})")] - private static partial void LogFullSearchResults( - ILogger logger, - int pageSize, - int pageNumber, - string searchQuery, - bool isSemantic, - [LogProperties] FullSearchResultsLogProperties result); - - private sealed record FullSearchResultsLogProperties(string[] Urls); -} - -/// -/// API request model for full-page search. -/// -public record FullSearchApiRequest -{ - public required string Query { get; init; } - public int PageNumber { get; init; } = 1; - public int PageSize { get; init; } = 20; - public string[]? TypeFilter { get; init; } - public string[]? SectionFilter { get; init; } - public string[]? DeploymentFilter { get; init; } - public string[]? ProductFilter { get; init; } - public string? VersionFilter { get; init; } - public string SortBy { get; init; } = "relevance"; -} - -/// -/// API response model for full-page search. -/// -public record FullSearchApiResponse -{ - public required IEnumerable Results { get; init; } - public required int TotalResults { get; init; } - public required int PageNumber { get; init; } - public required int PageSize { get; init; } - public FullSearchAggregations Aggregations { get; init; } = new(); - public bool IsSemanticQuery { get; init; } - public int PageCount => TotalResults > 0 - ? (int)Math.Ceiling((double)TotalResults / PageSize) - : 0; -} diff --git a/src/api/Elastic.Documentation.Api.Core/Search/INavigationSearchGateway.cs b/src/api/Elastic.Documentation.Api.Core/Search/INavigationSearchGateway.cs deleted file mode 100644 index 02eb092778..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/Search/INavigationSearchGateway.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -namespace Elastic.Documentation.Api.Core.Search; - -public interface INavigationSearchGateway -{ - Task NavigationSearchAsync( - string query, - int pageNumber, - int pageSize, - string? filter = null, - Cancel ctx = default - ); -} - -public record NavigationSearchResult -{ - public required int TotalHits { get; init; } - public required List Results { get; init; } - public IReadOnlyDictionary Aggregations { get; init; } = new Dictionary(); -} diff --git a/src/api/Elastic.Documentation.Api.Core/Search/NavigationSearchUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Search/NavigationSearchUsecase.cs deleted file mode 100644 index 87d0ef695a..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/Search/NavigationSearchUsecase.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using Microsoft.Extensions.Logging; - -namespace Elastic.Documentation.Api.Core.Search; - -// note still called SearchUseCase because we'll re-add Search() and ensure both share the same client. -public partial class NavigationSearchUsecase(INavigationSearchGateway navigationSearchGateway, ILogger logger) -{ - public async Task NavigationSearchAsync(NavigationSearchApiRequest request, Cancel ctx = default) - { - var result = await navigationSearchGateway.NavigationSearchAsync( - request.Query, - request.PageNumber, - request.PageSize, - request.TypeFilter, - ctx - ); - - var response = new NavigationSearchApiResponse - { - Results = result.Results, - TotalResults = result.TotalHits, - PageNumber = request.PageNumber, - PageSize = request.PageSize, - Aggregations = new NavigationSearchAggregations { Type = result.Aggregations } - }; - - LogNavigationSearchResults( - logger, - response.PageSize, - response.PageNumber, - request.Query, - new AutoCompleteResultsLogProperties(result.Results.Select(i => i.Url).ToArray()) - ); - - return response; - } - - [LoggerMessage(Level = LogLevel.Information, Message = "Navigation search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}'")] - private static partial void LogNavigationSearchResults(ILogger logger, int pageSize, int pageNumber, string searchQuery, [LogProperties] AutoCompleteResultsLogProperties result); - - private sealed record AutoCompleteResultsLogProperties(string[] Urls); -} - -public record NavigationSearchApiRequest -{ - public required string Query { get; init; } - public int PageNumber { get; init; } = 1; - public int PageSize { get; init; } = 20; - public string? TypeFilter { get; init; } -} - -public record NavigationSearchApiResponse -{ - public required IEnumerable Results { get; init; } - public required int TotalResults { get; init; } - public required int PageNumber { get; init; } - public required int PageSize { get; init; } - public NavigationSearchAggregations Aggregations { get; init; } = new(); - public int PageCount => TotalResults > 0 - ? (int)Math.Ceiling((double)TotalResults / PageSize) - : 0; -} - -public record NavigationSearchAggregations -{ - public IReadOnlyDictionary Type { get; init; } = new Dictionary(); -} - -public record NavigationSearchResultItemParent -{ - public required string Title { get; init; } - public required string Url { get; init; } -} - -public record NavigationSearchResultItem -{ - public required string Type { get; init; } - public required string Url { get; init; } - public required string Title { get; init; } - public required string Description { get; init; } - public required NavigationSearchResultItemParent[] Parents { get; init; } - public float Score { get; init; } -} diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs b/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs deleted file mode 100644 index 9c051a5794..0000000000 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyUsecase.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Diagnostics; - -namespace Elastic.Documentation.Api.Core.Telemetry; - -/// -/// Proxies OTLP telemetry from the frontend to the local ADOT Lambda Layer collector. -/// The ADOT layer handles authentication and forwarding to the backend. -/// -public class OtlpProxyUsecase(IOtlpGateway gateway) -{ - private static readonly ActivitySource ActivitySource = new(TelemetryConstants.OtlpProxySourceName); - - /// - /// Proxies OTLP data from the frontend to the local ADOT collector. - /// - /// The OTLP signal type (traces, logs, or metrics) - /// The raw OTLP payload (JSON or protobuf) - /// Content-Type header from the original request - /// Cancellation token - /// Result containing HTTP status code and response content - public async Task ProxyOtlp( - OtlpSignalType signalType, - Stream requestBody, - string contentType, - Cancel ctx = default) - { - using var activity = ActivitySource.StartActivity("forward otlp", ActivityKind.Client); - - // Forward to gateway - return await gateway.ForwardOtlp(signalType, requestBody, contentType, ctx); - } -} diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj b/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj deleted file mode 100644 index 80382f47ee..0000000000 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Elastic.Documentation.Api.Infrastructure.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net10.0 - enable - enable - true - $(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.Http.Generated - Elastic.Documentation.Api.Infrastructure - Elastic.Documentation.Api.Infrastructure - - - - - - - - - - - - - - - - - - - - diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs similarity index 96% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs index 6c182efde9..aba730b558 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderAskAiGateway.cs @@ -7,12 +7,12 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; -public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger logger) : IAskAiGateway +public class AgentBuilderAskAiGateway(HttpClient httpClient, KibanaOptions kibanaOptions, ILogger logger) : IAskAiService { /// /// Model name used by Agent Builder (from AgentId) diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderStreamTransformer.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderStreamTransformer.cs similarity index 97% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderStreamTransformer.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderStreamTransformer.cs index 9eed7d6e0f..a7e0e49679 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AgentBuilderStreamTransformer.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AgentBuilderStreamTransformer.cs @@ -3,10 +3,10 @@ // See the LICENSE file in the project root for more information using System.Text.Json; -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Transforms Agent Builder SSE events to canonical AskAiEvent format diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiGatewayFactory.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs similarity index 79% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiGatewayFactory.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs index ec55afc03d..4414370c3e 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiGatewayFactory.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiGatewayFactory.cs @@ -2,25 +2,25 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// -/// Factory that creates the appropriate IAskAiGateway based on the resolved provider +/// Factory that creates the appropriate IAskAiService based on the resolved provider /// public class AskAiGatewayFactory( IServiceProvider serviceProvider, AskAiProviderResolver providerResolver, - ILogger logger) : IAskAiGateway + ILogger logger) : IAskAiService { public async Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default) { var provider = providerResolver.ResolveProvider(); - IAskAiGateway gateway = provider switch + IAskAiService gateway = provider switch { "LlmGateway" => serviceProvider.GetRequiredService(), "AgentBuilder" => serviceProvider.GetRequiredService(), diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiProviderResolver.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiProviderResolver.cs similarity index 95% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiProviderResolver.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiProviderResolver.cs index 9c6791d24e..f92eadc930 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/AskAiProviderResolver.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/AskAiProviderResolver.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Resolves which AI provider to use based on HTTP headers diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs similarity index 95% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs index 99e20e5ec1..d01d751b4e 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/ElasticsearchAskAiMessageFeedbackGateway.cs @@ -5,18 +5,18 @@ using System.Text.Json.Serialization; using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch.Serialization; -using Elastic.Documentation.Api.Core; -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api; +using Elastic.Documentation.Api.AskAi; using Elastic.Documentation.Configuration; using Elastic.Transport; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Records Ask AI message feedback to Elasticsearch. /// -public sealed class ElasticsearchAskAiMessageFeedbackGateway : IAskAiMessageFeedbackGateway, IDisposable +public sealed class ElasticsearchAskAiMessageFeedbackGateway : IAskAiMessageFeedbackService, IDisposable { private readonly ElasticsearchClient _client; private readonly string _indexName; diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/KibanaOptions.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/KibanaOptions.cs similarity index 90% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/KibanaOptions.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/KibanaOptions.cs index 69a043f0a8..df335c3cfc 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/KibanaOptions.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/KibanaOptions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; public class KibanaOptions(IConfiguration configuration) { diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs similarity index 95% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs index 58194dc80c..fa8c90213e 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayAskAiGateway.cs @@ -5,12 +5,12 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Elastic.Documentation.Api.Core.AskAi; -using Elastic.Documentation.Api.Infrastructure.Gcp; +using Elastic.Documentation.Api.AskAi; +using Elastic.Documentation.Api.Gcp; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; -public class LlmGatewayAskAiGateway(HttpClient httpClient, IGcpIdTokenProvider tokenProvider, LlmGatewayOptions options) : IAskAiGateway +public class LlmGatewayAskAiGateway(HttpClient httpClient, IGcpIdTokenProvider tokenProvider, LlmGatewayOptions options) : IAskAiService { /// /// Model name used by LLM Gateway (from PlatformContext.UseCase) diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayOptions.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayOptions.cs similarity index 95% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayOptions.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayOptions.cs index 665eb850d8..4102ffbc8d 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayOptions.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayOptions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; public class LlmGatewayOptions(IConfiguration configuration) { diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayStreamTransformer.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayStreamTransformer.cs similarity index 98% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayStreamTransformer.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayStreamTransformer.cs index 7808049560..57aec70ee7 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/LlmGatewayStreamTransformer.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/LlmGatewayStreamTransformer.cs @@ -5,10 +5,10 @@ using System.Diagnostics; using System.IO.Pipelines; using System.Text.Json; -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Transforms LLM Gateway SSE events to canonical AskAiEvent format diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/SseParser.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/SseParser.cs similarity index 97% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/SseParser.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/SseParser.cs index 20fab1570d..5cd2cb6bbc 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/SseParser.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/SseParser.cs @@ -8,7 +8,7 @@ using System.Text; using System.Text.Json.Serialization; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Represents a parsed Server-Sent Event (SSE) diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerBase.cs similarity index 98% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerBase.cs index d71d0f44f9..f3871a2da3 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerBase.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerBase.cs @@ -6,11 +6,11 @@ using System.IO.Pipelines; using System.Text; using System.Text.Json; -using Elastic.Documentation.Api.Core; -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api; +using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Base class for stream transformers that handles common streaming logic diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerFactory.cs b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerFactory.cs similarity index 94% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerFactory.cs rename to src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerFactory.cs index d59d9c6952..7cbb9ff3c6 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/AskAi/StreamTransformerFactory.cs +++ b/src/api/Elastic.Documentation.Api/Adapters/AskAi/StreamTransformerFactory.cs @@ -2,11 +2,11 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +namespace Elastic.Documentation.Api.Adapters.AskAi; /// /// Factory that creates the appropriate IStreamTransformer based on the resolved provider diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiEvent.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiEvent.cs similarity index 98% rename from src/api/Elastic.Documentation.Api.Core/AskAi/AskAiEvent.cs rename to src/api/Elastic.Documentation.Api/AskAi/AskAiEvent.cs index 2ad3e003c3..bb77ea4baa 100644 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiEvent.cs +++ b/src/api/Elastic.Documentation.Api/AskAi/AskAiEvent.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; -namespace Elastic.Documentation.Api.Core.AskAi; +namespace Elastic.Documentation.Api.AskAi; /// /// Base class for all AskAI events streamed to the frontend diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackRequest.cs b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackRequest.cs similarity index 94% rename from src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackRequest.cs rename to src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackRequest.cs index 127159ca0a..1ab6752d41 100644 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/AskAiMessageFeedbackRequest.cs +++ b/src/api/Elastic.Documentation.Api/AskAi/AskAiMessageFeedbackRequest.cs @@ -4,7 +4,7 @@ using System.Text.Json.Serialization; -namespace Elastic.Documentation.Api.Core.AskAi; +namespace Elastic.Documentation.Api.AskAi; /// /// Request model for submitting feedback on a specific Ask AI message. diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs similarity index 86% rename from src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiGateway.cs rename to src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs index 07e5161961..4ab3e1f2f8 100644 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiGateway.cs +++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiGateway.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.AskAi; +namespace Elastic.Documentation.Api.AskAi; /// /// Response from an AI gateway containing the stream and conversation metadata @@ -15,7 +15,9 @@ namespace Elastic.Documentation.Api.Core.AskAi; /// public record AskAiGatewayResponse(Stream Stream, Guid? GeneratedConversationId); -public interface IAskAiGateway +public record AskAiRequest(string Message, Guid? ConversationId); + +public interface IAskAiService { Task AskAi(AskAiRequest askAiRequest, Cancel ctx = default); } diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiMessageFeedbackGateway.cs b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs similarity index 84% rename from src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiMessageFeedbackGateway.cs rename to src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs index e3c52da60d..9f3ca07407 100644 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/IAskAiMessageFeedbackGateway.cs +++ b/src/api/Elastic.Documentation.Api/AskAi/IAskAiMessageFeedbackGateway.cs @@ -2,13 +2,13 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.AskAi; +namespace Elastic.Documentation.Api.AskAi; /// -/// Gateway interface for recording Ask AI message feedback. +/// Service interface for recording Ask AI message feedback. /// Infrastructure implementations may use different storage backends (Elasticsearch, database, etc.) /// -public interface IAskAiMessageFeedbackGateway +public interface IAskAiMessageFeedbackService { /// /// Records feedback for a specific Ask AI message. diff --git a/src/api/Elastic.Documentation.Api.Core/AskAi/IStreamTransformer.cs b/src/api/Elastic.Documentation.Api/AskAi/IStreamTransformer.cs similarity index 96% rename from src/api/Elastic.Documentation.Api.Core/AskAi/IStreamTransformer.cs rename to src/api/Elastic.Documentation.Api/AskAi/IStreamTransformer.cs index 6bda052977..f1a20eb8bb 100644 --- a/src/api/Elastic.Documentation.Api.Core/AskAi/IStreamTransformer.cs +++ b/src/api/Elastic.Documentation.Api/AskAi/IStreamTransformer.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.AskAi; +namespace Elastic.Documentation.Api.AskAi; /// /// Transforms raw SSE streams from various AI gateways into canonical AskAiEvent format diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/IParameterProvider.cs b/src/api/Elastic.Documentation.Api/Aws/IParameterProvider.cs similarity index 90% rename from src/api/Elastic.Documentation.Api.Infrastructure/Aws/IParameterProvider.cs rename to src/api/Elastic.Documentation.Api/Aws/IParameterProvider.cs index b911b856dc..887dd12341 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/IParameterProvider.cs +++ b/src/api/Elastic.Documentation.Api/Aws/IParameterProvider.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Infrastructure.Aws; +namespace Elastic.Documentation.Api.Aws; /// /// Abstraction for retrieving configuration parameters. diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LambdaExtensionParameterProvider.cs b/src/api/Elastic.Documentation.Api/Aws/LambdaExtensionParameterProvider.cs similarity index 97% rename from src/api/Elastic.Documentation.Api.Infrastructure/Aws/LambdaExtensionParameterProvider.cs rename to src/api/Elastic.Documentation.Api/Aws/LambdaExtensionParameterProvider.cs index 37bb8519fb..80b600ac49 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LambdaExtensionParameterProvider.cs +++ b/src/api/Elastic.Documentation.Api/Aws/LambdaExtensionParameterProvider.cs @@ -7,7 +7,7 @@ using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Aws; +namespace Elastic.Documentation.Api.Aws; public class LambdaExtensionParameterProvider(IHttpClientFactory httpClientFactory, AppEnvironment appEnvironment, ILogger logger) : IParameterProvider { diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs b/src/api/Elastic.Documentation.Api/Aws/LocalParameterProvider.cs similarity index 97% rename from src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs rename to src/api/Elastic.Documentation.Api/Aws/LocalParameterProvider.cs index 8ec6d6d18e..e63b92a9c8 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Aws/LocalParameterProvider.cs +++ b/src/api/Elastic.Documentation.Api/Aws/LocalParameterProvider.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; -namespace Elastic.Documentation.Api.Infrastructure.Aws; +namespace Elastic.Documentation.Api.Aws; public class LocalParameterProvider : IParameterProvider { diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/CacheKey.cs b/src/api/Elastic.Documentation.Api/Caching/CacheKey.cs similarity index 96% rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/CacheKey.cs rename to src/api/Elastic.Documentation.Api/Caching/CacheKey.cs index 67e565463a..d7f39b361a 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/CacheKey.cs +++ b/src/api/Elastic.Documentation.Api/Caching/CacheKey.cs @@ -5,7 +5,7 @@ using System.Security.Cryptography; using System.Text; -namespace Elastic.Documentation.Api.Infrastructure.Caching; +namespace Elastic.Documentation.Api.Caching; /// /// Represents a cache key with automatic hashing of sensitive identifiers. diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/DynamoDbDistributedCache.cs b/src/api/Elastic.Documentation.Api/Caching/DynamoDbDistributedCache.cs similarity index 98% rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/DynamoDbDistributedCache.cs rename to src/api/Elastic.Documentation.Api/Caching/DynamoDbDistributedCache.cs index e68cdf6791..f3f3ba940a 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/DynamoDbDistributedCache.cs +++ b/src/api/Elastic.Documentation.Api/Caching/DynamoDbDistributedCache.cs @@ -6,10 +6,10 @@ using System.Globalization; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; -using Elastic.Documentation.Api.Core; +using Elastic.Documentation.Api; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Caching; +namespace Elastic.Documentation.Api.Caching; /// /// DynamoDB implementation of for Lambda environments. diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/IDistributedCache.cs b/src/api/Elastic.Documentation.Api/Caching/IDistributedCache.cs similarity index 96% rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/IDistributedCache.cs rename to src/api/Elastic.Documentation.Api/Caching/IDistributedCache.cs index de875cb36f..4920b4820b 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/IDistributedCache.cs +++ b/src/api/Elastic.Documentation.Api/Caching/IDistributedCache.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Infrastructure.Caching; +namespace Elastic.Documentation.Api.Caching; /// /// Abstraction for distributed caching across Lambda invocations. diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/InMemoryDistributedCache.cs b/src/api/Elastic.Documentation.Api/Caching/InMemoryDistributedCache.cs similarity index 96% rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/InMemoryDistributedCache.cs rename to src/api/Elastic.Documentation.Api/Caching/InMemoryDistributedCache.cs index 9733ff574f..78e714d66d 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/InMemoryDistributedCache.cs +++ b/src/api/Elastic.Documentation.Api/Caching/InMemoryDistributedCache.cs @@ -4,7 +4,7 @@ using System.Collections.Concurrent; -namespace Elastic.Documentation.Api.Infrastructure.Caching; +namespace Elastic.Documentation.Api.Caching; /// /// In-memory implementation of for local development. diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/MultiLayerCache.cs b/src/api/Elastic.Documentation.Api/Caching/MultiLayerCache.cs similarity index 97% rename from src/api/Elastic.Documentation.Api.Infrastructure/Caching/MultiLayerCache.cs rename to src/api/Elastic.Documentation.Api/Caching/MultiLayerCache.cs index 273dd1edb7..0f59aa42cd 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Caching/MultiLayerCache.cs +++ b/src/api/Elastic.Documentation.Api/Caching/MultiLayerCache.cs @@ -4,10 +4,10 @@ using System.Collections.Concurrent; using System.Diagnostics; -using Elastic.Documentation.Api.Core; +using Elastic.Documentation.Api; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Caching; +namespace Elastic.Documentation.Api.Caching; /// /// Multi-layer cache decorator implementing L1 (in-memory) + L2 (distributed) caching strategy. diff --git a/src/api/Elastic.Documentation.Api.App/Dockerfile b/src/api/Elastic.Documentation.Api/Dockerfile similarity index 80% rename from src/api/Elastic.Documentation.Api.App/Dockerfile rename to src/api/Elastic.Documentation.Api/Dockerfile index 4912aaf0e9..52b9699870 100644 --- a/src/api/Elastic.Documentation.Api.App/Dockerfile +++ b/src/api/Elastic.Documentation.Api/Dockerfile @@ -29,14 +29,14 @@ ENV DOTNET_NOLOGO=true \ RUN arch=$TARGETARCH \ && if [ "$arch" = "amd64" ]; then arch="x64"; fi \ - && echo $TARGETOS-$arch > /tmp/rid - -RUN dotnet publish src/api/Elastic.Documentation.Api.App -r linux-x64 -c Release + && RID="${TARGETOS}-${arch}" \ + && dotnet publish src/api/Elastic.Documentation.Api -r "${RID}" -c Release \ + && cp -r ".artifacts/publish/Elastic.Documentation.Api/release_${RID}/." /app/publish/ # Runtime stage FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal AS runtime WORKDIR /app -COPY --from=base /app/.artifacts/publish/Elastic.Documentation.Api.App/release_linux-x64/ /app/ +COPY --from=base /app/publish/ /app/ RUN chmod +x /app/docs-builder-api EXPOSE 8080 ENV ASPNETCORE_URLS=http://+:8080 diff --git a/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj new file mode 100644 index 0000000000..26d0be4ac5 --- /dev/null +++ b/src/api/Elastic.Documentation.Api/Elastic.Documentation.Api.csproj @@ -0,0 +1,42 @@ + + + Exe + net10.0 + enable + enable + docs-builder-api + Elastic.Documentation.Api + true + $(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.Http.Generated + true + true + true + true + false + true + Linux + + + + + + + + + + + + + + + + + + + <_Parameter1>Elastic.Documentation.Api.Tests + + + <_Parameter1>Elastic.Documentation.Api.IntegrationTests + + + diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/GcpIdTokenProvider.cs b/src/api/Elastic.Documentation.Api/Gcp/GcpIdTokenProvider.cs similarity index 98% rename from src/api/Elastic.Documentation.Api.Infrastructure/Gcp/GcpIdTokenProvider.cs rename to src/api/Elastic.Documentation.Api/Gcp/GcpIdTokenProvider.cs index bb731d8f85..2a864af938 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/GcpIdTokenProvider.cs +++ b/src/api/Elastic.Documentation.Api/Gcp/GcpIdTokenProvider.cs @@ -6,9 +6,9 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using Elastic.Documentation.Api.Infrastructure.Caching; +using Elastic.Documentation.Api.Caching; -namespace Elastic.Documentation.Api.Infrastructure.Gcp; +namespace Elastic.Documentation.Api.Gcp; // This is a custom implementation to create an ID token for GCP. // Because Google.Api.Auth.OAuth2 is not compatible with AOT diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/IGcpIdTokenProvider.cs b/src/api/Elastic.Documentation.Api/Gcp/IGcpIdTokenProvider.cs similarity index 93% rename from src/api/Elastic.Documentation.Api.Infrastructure/Gcp/IGcpIdTokenProvider.cs rename to src/api/Elastic.Documentation.Api/Gcp/IGcpIdTokenProvider.cs index acfd64d3fd..48190d1e52 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Gcp/IGcpIdTokenProvider.cs +++ b/src/api/Elastic.Documentation.Api/Gcp/IGcpIdTokenProvider.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Infrastructure.Gcp; +namespace Elastic.Documentation.Api.Gcp; /// /// Interface for generating GCP ID tokens. diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs similarity index 54% rename from src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs rename to src/api/Elastic.Documentation.Api/MappingsExtensions.cs index 36db7ca1b1..8491c755bb 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/MappingsExtensions.cs +++ b/src/api/Elastic.Documentation.Api/MappingsExtensions.cs @@ -2,17 +2,18 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Api.Core.AskAi; -using Elastic.Documentation.Api.Core.Changes; -using Elastic.Documentation.Api.Core.Search; -using Elastic.Documentation.Api.Core.Telemetry; -using Elastic.Documentation.Configuration.Products; +using System.Diagnostics; +using System.Text.Json; +using Elastic.Documentation.Api.AskAi; +using Elastic.Documentation.Api.Telemetry; +using Elastic.Documentation.Search; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure; +namespace Elastic.Documentation.Api; public static class MappingsExtension { @@ -32,23 +33,71 @@ public static void MapElasticDocsApiEndpoints(this IEndpointRouteBuilder group, private static void MapAskAiEndpoint(IEndpointRouteBuilder group) { var askAiGroup = group.MapGroup("/ask-ai"); - _ = askAiGroup.MapPost("/stream", async (HttpContext context, AskAiRequest askAiRequest, AskAiUsecase askAiUsecase, Cancel ctx) => + _ = askAiGroup.MapPost("/stream", async (HttpContext context, AskAiRequest askAiRequest, IAskAiService askAiService, IStreamTransformer streamTransformer, ILogger logger, Cancel ctx) => { context.Response.ContentType = "text/event-stream"; context.Response.Headers.CacheControl = "no-cache"; context.Response.Headers.Connection = "keep-alive"; - var stream = await askAiUsecase.AskAi(askAiRequest, ctx); - await stream.CopyToAsync(context.Response.Body, ctx); + var askAiActivitySource = new ActivitySource(TelemetryConstants.AskAiSourceName); + logger.LogInformation("Starting AskAI chat with {AgentProvider} and {AgentId}", streamTransformer.AgentProvider, streamTransformer.AgentId); + var activity = askAiActivitySource.StartActivity($"chat {streamTransformer.AgentProvider}", ActivityKind.Client); + _ = activity?.SetTag("gen_ai.operation.name", "chat"); + _ = activity?.SetTag("gen_ai.provider.name", streamTransformer.AgentProvider); + _ = activity?.SetTag("gen_ai.agent.id", streamTransformer.AgentId); + if (askAiRequest.ConversationId is not null) + _ = activity?.SetTag("gen_ai.conversation.id", askAiRequest.ConversationId.ToString()); + + var inputMessages = new[] + { + new InputMessage("user", [new MessagePart("text", askAiRequest.Message)]) + }; + var inputMessagesJson = JsonSerializer.Serialize(inputMessages, ApiJsonContext.Default.InputMessageArray); + _ = activity?.SetTag("gen_ai.input.messages", inputMessagesJson); + var sanitizedMessage = askAiRequest.Message?.Replace("\r", "").Replace("\n", ""); + logger.LogInformation("AskAI input message: <{ask_ai.input.message}>", sanitizedMessage); + logger.LogInformation("Streaming AskAI response"); + + var response = await askAiService.AskAi(askAiRequest, ctx); + + var conversationId = response.GeneratedConversationId ?? askAiRequest.ConversationId; + if (conversationId is not null) + _ = activity?.SetTag("gen_ai.conversation.id", conversationId.ToString()); + + var transformedStream = await streamTransformer.TransformAsync( + response.Stream, + response.GeneratedConversationId, + activity, + ctx); + await transformedStream.CopyToAsync(context.Response.Body, ctx); }); // UUID validation is automatic via Guid type deserialization (returns 400 if invalid) - _ = askAiGroup.MapPost("/message-feedback", async (HttpContext context, AskAiMessageFeedbackRequest request, AskAiMessageFeedbackUsecase feedbackUsecase, Cancel ctx) => + _ = askAiGroup.MapPost("/message-feedback", async (HttpContext context, AskAiMessageFeedbackRequest request, IAskAiMessageFeedbackService feedbackService, ILogger logger, Cancel ctx) => { // Extract euid cookie for user tracking _ = context.Request.Cookies.TryGetValue("euid", out var euid); - await feedbackUsecase.SubmitFeedback(request, euid, ctx); + var feedbackActivitySource = new ActivitySource(TelemetryConstants.AskAiFeedbackSourceName); + using var activity = feedbackActivitySource.StartActivity("record message-feedback", ActivityKind.Internal); + _ = activity?.SetTag("gen_ai.conversation.id", request.ConversationId); + _ = activity?.SetTag("ask_ai.message.id", request.MessageId); + _ = activity?.SetTag("ask_ai.feedback.reaction", request.Reaction.ToString().ToLowerInvariant()); + + logger.LogInformation( + "Recording message feedback for message {MessageId} in conversation {ConversationId}: {Reaction}", + request.MessageId, + request.ConversationId, + request.Reaction); + + var record = new AskAiMessageFeedbackRecord( + request.MessageId, + request.ConversationId, + request.Reaction, + euid + ); + + await feedbackService.RecordFeedbackAsync(record, ctx); return Results.NoContent(); }).DisableAntiforgery(); } @@ -61,17 +110,17 @@ private static void MapNavigationSearch(IEndpointRouteBuilder group) [FromQuery(Name = "q")] string query, [FromQuery(Name = "page")] int? pageNumber, [FromQuery(Name = "type")] string? typeFilter, - NavigationSearchUsecase navigationSearchUsecase, + INavigationSearchService navigationSearchService, Cancel ctx ) => { - var request = new NavigationSearchApiRequest + var request = new NavigationSearchRequest { Query = query, PageNumber = pageNumber ?? 1, TypeFilter = typeFilter }; - var response = await navigationSearchUsecase.NavigationSearchAsync(request, ctx); + var response = await navigationSearchService.NavigationSearchAsync(request, ctx); return Results.Ok(response); }); } @@ -90,11 +139,11 @@ private static void MapFullSearch(IEndpointRouteBuilder group) [FromQuery(Name = "product")] string[]? productFilter, [FromQuery(Name = "version")] string? versionFilter, [FromQuery(Name = "sort")] string? sortBy, - FullSearchUsecase searchUsecase, + IFullSearchService searchService, Cancel ctx ) => { - var request = new FullSearchApiRequest + var request = new FullSearchRequest { Query = query, PageNumber = pageNumber ?? 1, @@ -106,7 +155,7 @@ Cancel ctx VersionFilter = versionFilter, SortBy = sortBy ?? "relevance" }; - var response = await searchUsecase.SearchAsync(request, ctx); + var response = await searchService.SearchAsync(request, ctx); return Results.Ok(response); }); } @@ -117,17 +166,17 @@ private static void MapChanges(IEndpointRouteBuilder group) => [FromQuery(Name = "since")] DateTimeOffset since, [FromQuery(Name = "cursor")] string? cursor, [FromQuery(Name = "size")] int? pageSize, - ChangesUsecase changesUsecase, + IChangesService changesService, Cancel ctx ) => { - var request = new ChangesApiRequest + var request = new ChangesRequest { Since = since, PageSize = pageSize ?? ChangesDefaults.PageSize, Cursor = cursor }; - var response = await changesUsecase.GetChangesAsync(request, ctx); + var response = await changesService.GetChangesAsync(request, ctx); return Results.Ok(response); }); @@ -146,10 +195,10 @@ private static void MapOtlpSignalEndpoint( string path, OtlpSignalType signalType) => group.MapPost(path, - async (HttpContext context, OtlpProxyUsecase proxyUsecase, Cancel ctx) => + async (HttpContext context, IOtlpService otlpService, Cancel ctx) => { var contentType = context.Request.ContentType ?? "application/json"; - var result = await proxyUsecase.ProxyOtlp( + var result = await otlpService.ForwardOtlp( signalType, context.Request.Body, contentType, diff --git a/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs b/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs new file mode 100644 index 0000000000..a0511fba66 --- /dev/null +++ b/src/api/Elastic.Documentation.Api/OpenTelemetry/OpenTelemetryExtensions.cs @@ -0,0 +1,96 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Documentation.ServiceDefaults.Telemetry; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry; +using OpenTelemetry.Logs; +using OpenTelemetry.Trace; + +namespace Elastic.Documentation.Api.OpenTelemetry; + +public static class OpenTelemetryExtensions +{ + /// + /// Configures Elastic OpenTelemetry (EDOT) for the Docs API. + /// Delegates euid enrichment and base configuration to AddEuidEnrichment, + /// then adds API-specific activity sources and path filters. + /// + /// The web application builder + /// The builder for chaining + public static TBuilder AddDocsApiOpenTelemetry(this TBuilder builder) + where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + if (!useOtlpExporter) + return builder; + + // Configure euid enrichment (euid processors, AspNetCore + HttpClient instrumentation, service.version) + _ = builder.AddEuidEnrichment(); + + // Additionally register the 5 API-specific activity sources + _ = builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => + { + _ = tracing + .AddSource(TelemetryConstants.AskAiSourceName) + .AddSource(TelemetryConstants.StreamTransformerSourceName) + .AddSource(TelemetryConstants.OtlpProxySourceName) + .AddSource(TelemetryConstants.CacheSourceName) + .AddSource(TelemetryConstants.AskAiFeedbackSourceName); + }); + + return builder; + } + + /// + /// Configures logging for the Docs API with euid enrichment. + /// This is the shared configuration used in both production and tests. + /// + public static global::OpenTelemetry.Logs.LoggerProviderBuilder AddDocsApiLogging(this global::OpenTelemetry.Logs.LoggerProviderBuilder builder) + { + _ = builder.AddProcessor(); + return builder; + } + + /// + /// Configures tracing for the Docs API with sources, instrumentation, and enrichment. + /// This is the shared configuration used in both production and tests. + /// + public static global::OpenTelemetry.Trace.TracerProviderBuilder AddDocsApiTracing(this global::OpenTelemetry.Trace.TracerProviderBuilder builder) + { + _ = builder + .AddSource(TelemetryConstants.AskAiSourceName) + .AddSource(TelemetryConstants.StreamTransformerSourceName) + .AddSource(TelemetryConstants.OtlpProxySourceName) + .AddSource(TelemetryConstants.CacheSourceName) + .AddSource(TelemetryConstants.AskAiFeedbackSourceName) + .AddAspNetCoreInstrumentation(aspNetCoreOptions => + { + // Don't trace root API endpoint (health check) + aspNetCoreOptions.Filter = (httpContext) => + { + var path = httpContext.Request.Path.Value ?? string.Empty; + // Exclude root API path: /docs/_api/v1 + return path != "/docs/_api/v1"; + }; + + // Enrich spans with custom attributes from HTTP context + aspNetCoreOptions.EnrichWithHttpRequest = (activity, httpRequest) => + { + // Add euid cookie value to span attributes and baggage + if (httpRequest.Cookies.TryGetValue("euid", out var euid) && !string.IsNullOrEmpty(euid)) + { + _ = activity.SetTag(ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid); + // Add to baggage so it propagates to all child spans + _ = activity.AddBaggage(ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName, euid); + } + }; + }) + .AddProcessor() // Automatically add euid to all child spans + .AddHttpClientInstrumentation(); + + return builder; + } +} diff --git a/src/api/Elastic.Documentation.Api.App/Program.cs b/src/api/Elastic.Documentation.Api/Program.cs similarity index 95% rename from src/api/Elastic.Documentation.Api.App/Program.cs rename to src/api/Elastic.Documentation.Api/Program.cs index 980fe483e0..bbee8a9201 100644 --- a/src/api/Elastic.Documentation.Api.App/Program.cs +++ b/src/api/Elastic.Documentation.Api/Program.cs @@ -2,8 +2,8 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Api.Infrastructure; -using Elastic.Documentation.Api.Infrastructure.OpenTelemetry; +using Elastic.Documentation.Api; +using Elastic.Documentation.Api.OpenTelemetry; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Search.Common; @@ -31,7 +31,7 @@ var environment = Environment.GetEnvironmentVariable("ENVIRONMENT"); Console.WriteLine($"Docs Environment: {environment}"); - builder.Services.AddElasticDocsApiUsecases(environment); + builder.Services.AddElasticDocsApiServices(environment); var app = builder.Build(); var logger = app.Services.GetRequiredService>(); diff --git a/src/api/Elastic.Documentation.Api.App/Properties/launchSettings.json b/src/api/Elastic.Documentation.Api/Properties/launchSettings.json similarity index 100% rename from src/api/Elastic.Documentation.Api.App/Properties/launchSettings.json rename to src/api/Elastic.Documentation.Api/Properties/launchSettings.json diff --git a/src/api/Elastic.Documentation.Api.Core/SerializationContext.cs b/src/api/Elastic.Documentation.Api/SerializationContext.cs similarity index 72% rename from src/api/Elastic.Documentation.Api.Core/SerializationContext.cs rename to src/api/Elastic.Documentation.Api/SerializationContext.cs index 5c28076f00..d5474683b9 100644 --- a/src/api/Elastic.Documentation.Api.Core/SerializationContext.cs +++ b/src/api/Elastic.Documentation.Api/SerializationContext.cs @@ -3,11 +3,10 @@ // See the LICENSE file in the project root for more information using System.Text.Json.Serialization; -using Elastic.Documentation.Api.Core.AskAi; -using Elastic.Documentation.Api.Core.Changes; -using Elastic.Documentation.Api.Core.Search; +using Elastic.Documentation.Api.AskAi; +using Elastic.Documentation.Search; -namespace Elastic.Documentation.Api.Core; +namespace Elastic.Documentation.Api; /// /// Types for OpenTelemetry telemetry serialization (AOT-compatible) @@ -21,19 +20,19 @@ public record OutputMessage(string Role, MessagePart[] Parts, string FinishReaso [JsonSerializable(typeof(AskAiRequest))] [JsonSerializable(typeof(AskAiMessageFeedbackRequest))] [JsonSerializable(typeof(Reaction))] -[JsonSerializable(typeof(NavigationSearchApiRequest))] -[JsonSerializable(typeof(NavigationSearchApiResponse))] +[JsonSerializable(typeof(NavigationSearchRequest))] +[JsonSerializable(typeof(NavigationSearchResponse))] [JsonSerializable(typeof(NavigationSearchAggregations))] [JsonSerializable(typeof(InputMessage))] [JsonSerializable(typeof(OutputMessage[]))] [JsonSerializable(typeof(MessagePart))] [JsonSerializable(typeof(InputMessage[]))] -[JsonSerializable(typeof(FullSearchApiRequest))] -[JsonSerializable(typeof(FullSearchApiResponse))] +[JsonSerializable(typeof(FullSearchRequest))] +[JsonSerializable(typeof(FullSearchResponse))] [JsonSerializable(typeof(FullSearchAggregations))] -[JsonSerializable(typeof(ChangesApiResponse))] +[JsonSerializable(typeof(ChangesResponse))] [JsonSerializable(typeof(ChangedPageDto))] [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] public partial class ApiJsonContext : JsonSerializerContext; diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs b/src/api/Elastic.Documentation.Api/ServicesExtension.cs similarity index 74% rename from src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs rename to src/api/Elastic.Documentation.Api/ServicesExtension.cs index e597e9e45f..4e846b57d7 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/ServicesExtension.cs +++ b/src/api/Elastic.Documentation.Api/ServicesExtension.cs @@ -4,20 +4,19 @@ using System.ComponentModel.DataAnnotations; using Amazon.DynamoDBv2; -using Elastic.Documentation.Api.Core; -using Elastic.Documentation.Api.Core.AskAi; -using Elastic.Documentation.Api.Core.Telemetry; -using Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; -using Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry; -using Elastic.Documentation.Api.Infrastructure.Caching; -using Elastic.Documentation.Api.Infrastructure.Gcp; +using Elastic.Documentation.Api; +using Elastic.Documentation.Api.Adapters.AskAi; +using Elastic.Documentation.Api.AskAi; +using Elastic.Documentation.Api.Caching; +using Elastic.Documentation.Api.Gcp; +using Elastic.Documentation.Api.Telemetry; using Elastic.Documentation.Search; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation.Api.Infrastructure; +namespace Elastic.Documentation.Api; [EnumExtensions] public enum AppEnv @@ -42,20 +41,20 @@ public static class ServicesExtension return loggerFactory?.CreateLogger(typeof(ServicesExtension)); } - public static void AddElasticDocsApiUsecases(this IServiceCollection services, string? appEnvironment) + public static void AddElasticDocsApiServices(this IServiceCollection services, string? appEnvironment) { if (AppEnvExtensions.TryParse(appEnvironment, out var parsedEnvironment, true)) - AddElasticDocsApiUsecases(services, parsedEnvironment); + AddElasticDocsApiServices(services, parsedEnvironment); else { var logger = GetLogger(services); logger?.LogWarning("Unable to parse environment {AppEnvironment} into AppEnvironment. Using default AppEnvironment.Dev", appEnvironment); - AddElasticDocsApiUsecases(services, AppEnv.Dev); + AddElasticDocsApiServices(services, AppEnv.Dev); } } - private static void AddElasticDocsApiUsecases(this IServiceCollection services, AppEnv appEnv) + private static void AddElasticDocsApiServices(this IServiceCollection services, AppEnv appEnv) { _ = services.ConfigureHttpJsonOptions(options => { @@ -72,9 +71,9 @@ private static void AddElasticDocsApiUsecases(this IServiceCollection services, // Register AppEnvironment as a singleton for dependency injection _ = services.AddSingleton(new AppEnvironment { Current = appEnv }); AddDistributedCache(services, appEnv); - AddAskAiUsecase(services, appEnv); - AddSearchUsecase(services, appEnv); - AddOtlpProxyUsecase(services, appEnv); + AddAskAiServices(services, appEnv); + AddSearchServices(services, appEnv); + AddOtlpProxyService(services, appEnv); } // Note: IParameterProvider is no longer needed - all options now read from IConfiguration (env vars) @@ -132,10 +131,10 @@ private static void AddDistributedCache(IServiceCollection services, AppEnv appE } } - private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv) + private static void AddAskAiServices(IServiceCollection services, AppEnv appEnv) { var logger = GetLogger(services); - logger?.LogInformation("Configuring AskAi use case for environment {AppEnvironment}", appEnv); + logger?.LogInformation("Configuring AskAi services for environment {AppEnvironment}", appEnv); try { @@ -151,9 +150,6 @@ private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv) _ = services.AddSingleton(); logger?.LogInformation("KibanaOptions registered successfully"); - _ = services.AddScoped(); - logger?.LogInformation("AskAiUsecase registered successfully"); - // Register HttpContextAccessor for provider resolution _ = services.AddHttpContextAccessor(); logger?.LogInformation("HttpContextAccessor registered successfully"); @@ -162,45 +158,46 @@ private static void AddAskAiUsecase(IServiceCollection services, AppEnv appEnv) _ = services.AddScoped(); logger?.LogInformation("AskAiProviderResolver registered successfully"); - // Register both gateways as concrete types + // Register both service implementations as concrete types _ = services.AddScoped(); _ = services.AddScoped(); - logger?.LogInformation("Both AI gateways registered as concrete types"); + logger?.LogInformation("Both AI service implementations registered as concrete types"); // Register both transformers as concrete types _ = services.AddScoped(); _ = services.AddScoped(); logger?.LogInformation("Both stream transformers registered as concrete types"); - // Register factories as interface implementations - _ = services.AddScoped(); + // Register factory as interface implementation + _ = services.AddScoped(); _ = services.AddScoped(); - logger?.LogInformation("Gateway and transformer factories registered successfully - provider switchable via X-AI-Provider header"); + logger?.LogInformation("Service and transformer factories registered successfully - provider switchable via X-AI-Provider header"); - // Register message feedback components (gateway is singleton for connection reuse) - _ = services.AddSingleton(); - _ = services.AddScoped(); - logger?.LogInformation("AskAiMessageFeedbackUsecase and Elasticsearch gateway registered successfully"); + // Register message feedback service (singleton for connection reuse) + _ = services.AddSingleton(); + logger?.LogInformation("AskAiMessageFeedbackService (Elasticsearch) registered successfully"); } catch (Exception ex) { - logger?.LogError(ex, "Failed to configure AskAi use case for environment {AppEnvironment}", appEnv); + logger?.LogError(ex, "Failed to configure AskAi services for environment {AppEnvironment}", appEnv); throw; } } - private static void AddSearchUsecase(IServiceCollection services, AppEnv appEnv) + + private static void AddSearchServices(IServiceCollection services, AppEnv appEnv) { var logger = GetLogger(services); - logger?.LogInformation("Configuring Search use case for environment {AppEnvironment}", appEnv); + logger?.LogInformation("Configuring Search services for environment {AppEnvironment}", appEnv); // Use the shared search service for DI registration _ = services.AddSearchServices(); + logger?.LogInformation("Full search service registered with hybrid RRF support"); } - private static void AddOtlpProxyUsecase(IServiceCollection services, AppEnv appEnv) + private static void AddOtlpProxyService(IServiceCollection services, AppEnv appEnv) { var logger = GetLogger(services); - logger?.LogInformation("Configuring OTLP proxy use case for environment {AppEnvironment}", appEnv); + logger?.LogInformation("Configuring OTLP proxy service for environment {AppEnvironment}", appEnv); _ = services.AddSingleton(sp => { @@ -209,14 +206,13 @@ private static void AddOtlpProxyUsecase(IServiceCollection services, AppEnv appE }); // Register named HttpClient for OTLP proxy - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigureHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(30); }); - _ = services.AddScoped(); - _ = services.AddScoped(); + _ = services.AddScoped(); logger?.LogInformation("OTLP proxy configured to forward to ADOT Lambda Layer collector"); } } diff --git a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs similarity index 87% rename from src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs rename to src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs index d2a573238a..0eede2c9a3 100644 --- a/src/api/Elastic.Documentation.Api.Infrastructure/Adapters/Telemetry/AdotOtlpGateway.cs +++ b/src/api/Elastic.Documentation.Api/Telemetry/AdotOtlpService.cs @@ -2,21 +2,22 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Diagnostics; using System.Net.Sockets; -using Elastic.Documentation.Api.Core.Telemetry; using Microsoft.Extensions.Logging; -namespace Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry; +namespace Elastic.Documentation.Api.Telemetry; /// -/// Gateway that forwards OTLP telemetry to the ADOT Lambda Layer collector. +/// Service that forwards OTLP telemetry to the ADOT Lambda Layer collector. /// -public class AdotOtlpGateway( +public class AdotOtlpService( IHttpClientFactory httpClientFactory, OtlpProxyOptions options, - ILogger logger) : IOtlpGateway + ILogger logger) : IOtlpService { public const string HttpClientName = "OtlpProxy"; + private static readonly ActivitySource ActivitySource = new(TelemetryConstants.OtlpProxySourceName); private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); /// @@ -26,6 +27,8 @@ public async Task ForwardOtlp( string contentType, Cancel ctx = default) { + using var activity = ActivitySource.StartActivity("forward otlp", ActivityKind.Client); + try { var targetUrl = $"{options.Endpoint.TrimEnd('/')}/v1/{signalType.ToStringFast(true)}"; diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs similarity index 85% rename from src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs rename to src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs index 7cb0a67d85..77fcb9c19e 100644 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/IOtlpGateway.cs +++ b/src/api/Elastic.Documentation.Api/Telemetry/IOtlpService.cs @@ -2,12 +2,12 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.Telemetry; +namespace Elastic.Documentation.Api.Telemetry; /// -/// Gateway for forwarding OTLP telemetry to a collector. +/// Service for forwarding OTLP telemetry to a collector. /// -public interface IOtlpGateway +public interface IOtlpService { /// /// Forwards OTLP telemetry data to the collector. diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpForwardResult.cs similarity index 93% rename from src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs rename to src/api/Elastic.Documentation.Api/Telemetry/OtlpForwardResult.cs index 7a9b4a1d0d..9acddc8fb7 100644 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpForwardResult.cs +++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpForwardResult.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.Telemetry; +namespace Elastic.Documentation.Api.Telemetry; /// /// Result of forwarding OTLP telemetry to a collector. diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs similarity index 83% rename from src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs rename to src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs index a684e60f8b..6e355de68f 100644 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyOptions.cs +++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyOptions.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Configuration; -namespace Elastic.Documentation.Api.Core.Telemetry; +namespace Elastic.Documentation.Api.Telemetry; /// /// Configuration options for the OTLP proxy. @@ -14,12 +14,12 @@ namespace Elastic.Documentation.Api.Core.Telemetry; /// ADOT Lambda Layer runs a local OpenTelemetry Collector that accepts OTLP/HTTP on: /// - localhost:4318 (HTTP/JSON and HTTP/protobuf) /// - localhost:4317 (gRPC) -/// +/// /// Configuration priority: /// 1. OtlpProxy:Endpoint in IConfiguration (for tests/overrides) /// 2. OTEL_EXPORTER_OTLP_ENDPOINT environment variable /// 3. Default: http://localhost:4318 -/// +/// /// The proxy will return 503 if the collector is not available. /// public class OtlpProxyOptions(IConfiguration configuration) @@ -33,16 +33,17 @@ public class OtlpProxyOptions(IConfiguration configuration) private static string ResolveEndpoint(IConfiguration configuration) { const string configKey = "OtlpProxy:Endpoint"; - const string envVarKey = "OTLP_PROXY_ENDPOINT"; const string defaultEndpoint = "http://localhost:4318"; // Priority 1: Explicit configuration (for tests or custom deployments) if (!string.IsNullOrEmpty(configuration[configKey])) return configuration[configKey]!; - // Priority 2: Environment variable (ADOT Lambda Layer standard) - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(envVarKey))) - return Environment.GetEnvironmentVariable(envVarKey)!; + // Priority 2: Standard OTEL env var, then legacy fallback + var endpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") + ?? Environment.GetEnvironmentVariable("OTLP_PROXY_ENDPOINT"); + if (!string.IsNullOrEmpty(endpoint)) + return endpoint; // Priority 3: Default (ADOT Lambda Layer collector) return defaultEndpoint; diff --git a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyRequest.cs b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyRequest.cs similarity index 95% rename from src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyRequest.cs rename to src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyRequest.cs index 6b79762ccd..3a5b48d5e0 100644 --- a/src/api/Elastic.Documentation.Api.Core/Telemetry/OtlpProxyRequest.cs +++ b/src/api/Elastic.Documentation.Api/Telemetry/OtlpProxyRequest.cs @@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations; using NetEscapades.EnumGenerators; -namespace Elastic.Documentation.Api.Core.Telemetry; +namespace Elastic.Documentation.Api.Telemetry; /// /// OTLP signal types supported by the proxy. diff --git a/src/api/Elastic.Documentation.Api.Core/TelemetryConstants.cs b/src/api/Elastic.Documentation.Api/TelemetryConstants.cs similarity index 82% rename from src/api/Elastic.Documentation.Api.Core/TelemetryConstants.cs rename to src/api/Elastic.Documentation.Api/TelemetryConstants.cs index 8619d885c0..5e33d70fc2 100644 --- a/src/api/Elastic.Documentation.Api.Core/TelemetryConstants.cs +++ b/src/api/Elastic.Documentation.Api/TelemetryConstants.cs @@ -2,13 +2,19 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core; +namespace Elastic.Documentation.Api; /// /// Constants for OpenTelemetry instrumentation in the Docs API. /// public static class TelemetryConstants { + /// + /// Tag/baggage name used to annotate spans with the user's EUID value. + /// Forwarded from ServiceDefaults.Telemetry.TelemetryConstants for convenience. + /// + public const string UserEuidAttributeName = ServiceDefaults.Telemetry.TelemetryConstants.UserEuidAttributeName; + /// /// ActivitySource name for AskAi operations. /// Used in AskAiUsecase to create spans. @@ -21,11 +27,6 @@ public static class TelemetryConstants /// public const string StreamTransformerSourceName = "Elastic.Documentation.Api.StreamTransformer"; - /// - /// Tag/baggage name used to annotate spans with the user's EUID value. - /// - public const string UserEuidAttributeName = "user.euid"; - /// /// ActivitySource name for OTLP proxy operations. /// Used to trace frontend telemetry proxying. @@ -43,10 +44,4 @@ public static class TelemetryConstants /// Used to trace feedback submissions. /// public const string AskAiFeedbackSourceName = "Elastic.Documentation.Api.AskAiFeedback"; - - /// - /// ActivitySource name for MCP remote tool call operations. - /// Used to trace MCP tool execution and outcomes. - /// - public const string McpToolSourceName = "Elastic.Documentation.Api.McpTools"; } diff --git a/src/api/Elastic.Documentation.Api.Core/Validation/IValidator.cs b/src/api/Elastic.Documentation.Api/Validation/IValidator.cs similarity index 86% rename from src/api/Elastic.Documentation.Api.Core/Validation/IValidator.cs rename to src/api/Elastic.Documentation.Api/Validation/IValidator.cs index 1aa281764d..3948cd2783 100644 --- a/src/api/Elastic.Documentation.Api.Core/Validation/IValidator.cs +++ b/src/api/Elastic.Documentation.Api/Validation/IValidator.cs @@ -2,7 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.Validation; +namespace Elastic.Documentation.Api.Validation; public interface IValidator { diff --git a/src/api/Elastic.Documentation.Api.App/appsettings.development.json b/src/api/Elastic.Documentation.Api/appsettings.Development.json similarity index 100% rename from src/api/Elastic.Documentation.Api.App/appsettings.development.json rename to src/api/Elastic.Documentation.Api/appsettings.Development.json diff --git a/src/api/Elastic.Documentation.Api.App/appsettings.edge.json b/src/api/Elastic.Documentation.Api/appsettings.edge.json similarity index 100% rename from src/api/Elastic.Documentation.Api.App/appsettings.edge.json rename to src/api/Elastic.Documentation.Api/appsettings.edge.json diff --git a/src/api/Elastic.Documentation.Api.App/appsettings.json b/src/api/Elastic.Documentation.Api/appsettings.json similarity index 100% rename from src/api/Elastic.Documentation.Api.App/appsettings.json rename to src/api/Elastic.Documentation.Api/appsettings.json diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj index ba0f765322..f5a5b5bd5d 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj +++ b/src/api/Elastic.Documentation.Mcp.Remote/Elastic.Documentation.Mcp.Remote.csproj @@ -21,10 +21,8 @@ - + - - diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs index c994a65ba5..2549f5b6ca 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/McpBearerAuthMiddleware.cs @@ -4,6 +4,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Microsoft.IdentityModel.Tokens; diff --git a/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs b/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs index 3a0ec7e6ef..74130afdcd 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/McpOAuthMetadata.cs @@ -4,6 +4,7 @@ using System.Security.Cryptography; using System.Text.Json.Serialization; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs index 6e8100c5cb..291c61f9fc 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Program.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Program.cs @@ -2,15 +2,16 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Api.Infrastructure.OpenTelemetry; using Elastic.Documentation.Assembler.Links; using Elastic.Documentation.Assembler.Mcp; using Elastic.Documentation.Configuration; using Elastic.Documentation.LinkIndex; using Elastic.Documentation.Links.InboundLinks; using Elastic.Documentation.Mcp.Remote; +using Elastic.Documentation.Mcp.Remote.Telemetry; using Elastic.Documentation.Search.Common; using Elastic.Documentation.ServiceDefaults; +using Elastic.Documentation.ServiceDefaults.Telemetry; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics.HealthChecks; @@ -18,13 +19,16 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ModelContextProtocol; +using OpenTelemetry.Trace; try { var builder = WebApplication.CreateSlimBuilder(args); _ = builder.AddDocumentationServiceDefaults(); _ = builder.AddDefaultHealthChecks(); - _ = builder.AddDocsApiOpenTelemetry(); + _ = builder.AddEuidEnrichment(); + _ = builder.Services.ConfigureOpenTelemetryTracerProvider(t => + t.AddSource(McpToolTelemetry.McpToolSourceName)); // Configure Kestrel to listen on port 8080 (standard container port) _ = builder.WebHost.ConfigureKestrel(serverOptions => diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs b/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs index 5f1f37243c..3aa2424060 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Telemetry/McpToolTelemetry.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Reflection; -using Elastic.Documentation.Api.Core; using Elastic.Documentation.Configuration; using Microsoft.Extensions.Logging; @@ -12,7 +11,8 @@ namespace Elastic.Documentation.Mcp.Remote.Telemetry; public static class McpToolTelemetry { - private static readonly ActivitySource McpActivitySource = new(TelemetryConstants.McpToolSourceName); + internal const string McpToolSourceName = "Elastic.Documentation.Api.McpTools"; + private static readonly ActivitySource McpActivitySource = new(McpToolSourceName); private static readonly McpServerProfile ServerProfile = ResolveServerProfile(); private static readonly string? ServerVersion = ResolveServerVersion(); diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs index 3c13089eb1..49a6ede81b 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/CoherenceTools.cs @@ -5,10 +5,10 @@ using System.ComponentModel; using System.Diagnostics; using System.Text.Json; -using Elastic.Documentation.Api.Core.Search; using Elastic.Documentation.Assembler.Mcp; using Elastic.Documentation.Mcp.Remote.Responses; using Elastic.Documentation.Mcp.Remote.Telemetry; +using Elastic.Documentation.Search; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; @@ -18,7 +18,7 @@ namespace Elastic.Documentation.Mcp.Remote.Tools; /// MCP tools for checking documentation coherence and finding inconsistencies. /// [McpServerToolType] -public class CoherenceTools(IFullSearchGateway fullSearchGateway, ILogger logger) +public class CoherenceTools(IFullSearchService fullSearchGateway, ILogger logger) { /// /// Checks documentation coherence for a given topic. @@ -73,13 +73,13 @@ public async Task CheckCoherence( var response = new CoherenceCheckResponse { Topic = topic, - TotalDocuments = result.TotalHits, + TotalDocuments = result.TotalResults, AnalyzedDocuments = result.Results.Count, SectionCoverage = navigationSections, ProductCoverage = products, DocsWithAiSummary = docsWithSummaries, DocsWithRagSummary = docsWithRagSummaries, - CoverageScore = CalculateCoverageScore(result.TotalHits, navigationSections.Count, products.Count), + CoverageScore = CalculateCoverageScore(result.TotalResults, navigationSections.Count, products.Count), TopDocuments = result.Results.Take(5).Select(r => new CoherenceDocDto { Url = r.Url, @@ -203,7 +203,7 @@ public async Task FindInconsistencies( { Topic = topic, FocusArea = focusArea, - TotalDocuments = result.TotalHits, + TotalDocuments = result.TotalResults, PotentialInconsistencies = potentialOverlaps.Take(10).ToList(), ProductBreakdown = byProduct.ToDictionary(g => g.Key, g => g.Value.Count) }; diff --git a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs index b85a12a4f6..021c245a20 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs +++ b/src/api/Elastic.Documentation.Mcp.Remote/Tools/SearchTools.cs @@ -5,10 +5,10 @@ using System.ComponentModel; using System.Diagnostics; using System.Text.Json; -using Elastic.Documentation.Api.Core.Search; using Elastic.Documentation.Assembler.Mcp; using Elastic.Documentation.Mcp.Remote.Responses; using Elastic.Documentation.Mcp.Remote.Telemetry; +using Elastic.Documentation.Search; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; @@ -18,7 +18,7 @@ namespace Elastic.Documentation.Mcp.Remote.Tools; /// MCP tools for semantic search operations on Elastic documentation. /// [McpServerToolType] -public class SearchTools(IFullSearchGateway fullSearchGateway, ILogger logger) +public class SearchTools(IFullSearchService fullSearchGateway, ILogger logger) { /// /// Performs semantic search across all Elastic documentation. @@ -70,7 +70,7 @@ public async Task SemanticSearch( var response = new SemanticSearchResponse { Query = query, - TotalHits = result.TotalHits, + TotalHits = result.TotalResults, IsSemanticQuery = result.IsSemanticQuery, Results = result.Results.Select(r => new SearchResultDto { diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs index 4120b13a25..cf171799a8 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs @@ -6,7 +6,6 @@ using System.IO.Abstractions; using System.Text; using System.Text.RegularExpressions; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Utilities; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Assembler; diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs index bd1adbe4e8..cf0b061e21 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs @@ -6,7 +6,6 @@ using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; using Elastic.Changelog.Rendering; using Elastic.Changelog.Utilities; diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs index 3f66938565..2f867e3f1a 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogRemoveService.cs @@ -4,7 +4,6 @@ using System.IO.Abstractions; using System.Linq; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; diff --git a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs index 6d247f2a1a..7462239ee1 100644 --- a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs +++ b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs @@ -3,8 +3,8 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.Inference; diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs index a6edeb8368..f140088ad8 100644 --- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs +++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrEvaluationService.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO.Abstractions; using Actions.Core.Services; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; using Elastic.Documentation.Configuration; diff --git a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs index 4308bf1e2c..aa932859dd 100644 --- a/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs +++ b/src/services/Elastic.Changelog/Evaluation/ChangelogPrepareArtifactService.cs @@ -6,9 +6,9 @@ using System.Text; using System.Text.Json; using Actions.Core.Services; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Utilities; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.Services; using Microsoft.Extensions.Logging; diff --git a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs index 2351414d40..1784f98dcb 100644 --- a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs +++ b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs @@ -6,7 +6,6 @@ using System.IO.Abstractions; using System.Text; using Elastic.Changelog.Bundling; -using Elastic.Changelog.Configuration; using Elastic.Changelog.GitHub; using Elastic.Changelog.Utilities; using Elastic.Documentation; diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs index c5d400f046..f59297ac40 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/ChangelogAsciidocRenderer.cs @@ -7,7 +7,7 @@ using System.IO.Abstractions; using System.Text; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Asciidoc; diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs index 0c8188b41b..81a895618f 100644 --- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs +++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs @@ -5,13 +5,13 @@ using System.ComponentModel.DataAnnotations; using System.IO.Abstractions; using System.Text.Json.Serialization; -using Elastic.Changelog.Configuration; using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; using Elastic.Documentation.Services; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using NetEscapades.EnumGenerators; using Nullean.ScopedFileSystem; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs index 0434f2c902..1eae9745d4 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/BreakingChangesMarkdownRenderer.cs @@ -8,7 +8,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs index 38a4afe3e5..9d0e40cfdd 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/ChangelogGfmRenderer.cs @@ -8,7 +8,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs index 048612f32d..43843594d8 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/DeprecationsMarkdownRenderer.cs @@ -7,7 +7,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs index 73f92dee39..4531e47f9a 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs @@ -8,7 +8,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs index 5b9dbe521a..b84e4945e7 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/KnownIssuesMarkdownRenderer.cs @@ -7,7 +7,7 @@ using Elastic.Documentation.ReleaseNotes; using Nullean.ScopedFileSystem; using static System.Globalization.CultureInfo; -using static Elastic.Documentation.ChangelogEntryType; +using static Elastic.Documentation.ReleaseNotes.ChangelogEntryType; namespace Elastic.Changelog.Rendering.Markdown; diff --git a/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs b/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs deleted file mode 100644 index 1599702cfb..0000000000 --- a/src/services/Elastic.Changelog/Serialization/ChangelogYamlStaticContext.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using YamlDotNet.Serialization; - -namespace Elastic.Changelog.Serialization; - -[YamlStaticContext] -// YAML DTOs for CLI configuration (changelog.yml) -[YamlSerializable(typeof(ChangelogConfigurationYaml))] -[YamlSerializable(typeof(PivotConfigurationYaml))] -[YamlSerializable(typeof(TypeEntryYaml))] -[YamlSerializable(typeof(RulesConfigurationYaml))] -[YamlSerializable(typeof(CreateRulesYaml))] -[YamlSerializable(typeof(BundleRulesYaml))] -[YamlSerializable(typeof(PublishRulesYaml))] -[YamlSerializable(typeof(ProductsConfigYaml))] -[YamlSerializable(typeof(DefaultProductYaml))] -[YamlSerializable(typeof(BundleConfigurationYaml))] -[YamlSerializable(typeof(BundleProfileYaml))] -[YamlSerializable(typeof(ExtractConfigurationYaml))] -[YamlSerializable(typeof(YamlLenientList))] -public partial class ChangelogYamlStaticContext; diff --git a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs index b0baa6c81f..2c7f1bc838 100644 --- a/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs +++ b/src/services/Elastic.Changelog/Uploading/ChangelogUploadService.cs @@ -5,7 +5,7 @@ using System.IO.Abstractions; using System.Text.RegularExpressions; using Amazon.S3; -using Elastic.Changelog.Configuration; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Configuration.ReleaseNotes; diff --git a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs index bc9615e18d..73b60ff407 100644 --- a/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs +++ b/src/services/Elastic.Documentation.Assembler/Building/AssemblerBuildService.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using System.Text; using Actions.Core.Services; +using Elastic.Documentation; using Elastic.Documentation.Assembler.Navigation; using Elastic.Documentation.Assembler.Sourcing; using Elastic.Documentation.Configuration; diff --git a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs index 56da04d68b..18c621c8ad 100644 --- a/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs +++ b/src/services/Elastic.Documentation.Assembler/Navigation/GlobalNavigationService.cs @@ -43,7 +43,7 @@ public async Task ValidateLocalLinkReference(IDiagnosticsCollector collect var assembleContext = new AssembleContext(configuration, configurationContext, "dev", collector, fileSystem, fileSystem, null, null); var root = fileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); - var repository = GitCheckoutInformation.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName + var repository = GitCheckoutInformationFactory.Create(root, fileSystem, logFactory.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName ?? throw new Exception("Unable to determine repository name"); var namespaceChecker = new NavigationPrefixChecker(logFactory, assembleContext); diff --git a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs index 4bb4bcbc63..204292009f 100644 --- a/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs +++ b/src/services/Elastic.Documentation.Isolated/IsolatedBuildService.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions; using Actions.Core.Services; using Elastic.ApiExplorer; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Configuration.Inference; diff --git a/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs b/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs new file mode 100644 index 0000000000..93eb0bdf59 --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search.Contract/AppliesToEntry.cs @@ -0,0 +1,28 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text.Json.Serialization; + +namespace Elastic.Documentation.Search; + +/// +/// Flat wire-format entry for the applies_to array indexed in Elasticsearch. +/// One entry per (type, sub_type, lifecycle, version) tuple, preserving the exact JSON shape +/// produced by the applicability converter in docs-builder. +/// +public record AppliesToEntry +{ + [JsonPropertyName("type")] + public required string Type { get; init; } + + [JsonPropertyName("sub_type")] + public required string SubType { get; init; } + + [JsonPropertyName("lifecycle")] + public required string Lifecycle { get; init; } + + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Version { get; init; } +} diff --git a/src/Elastic.Documentation/Search/DocumentationDocument.cs b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs similarity index 98% rename from src/Elastic.Documentation/Search/DocumentationDocument.cs rename to src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs index 77e8f17da6..75244d6856 100644 --- a/src/Elastic.Documentation/Search/DocumentationDocument.cs +++ b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationDocument.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using System.Text.Json.Serialization; -using Elastic.Documentation.AppliesTo; using Elastic.Mapping; namespace Elastic.Documentation.Search; @@ -134,7 +133,7 @@ public string ContentType [Nested] [JsonPropertyName("applies_to")] - public ApplicableTo? Applies { get; set; } + public IReadOnlyCollection? Applies { get; set; } [JsonPropertyName("body")] public string? Body { get; set; } diff --git a/src/Elastic.Documentation/Search/DocumentationMappingConfig.cs b/src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs similarity index 100% rename from src/Elastic.Documentation/Search/DocumentationMappingConfig.cs rename to src/services/search/Elastic.Documentation.Search.Contract/DocumentationMappingConfig.cs diff --git a/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj b/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj new file mode 100644 index 0000000000..03a2f9155d --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search.Contract/Elastic.Documentation.Search.Contract.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + Elastic.Documentation.Search + true + true + $(NoWarn);CS1591;CS1573;CS1572;CS1571;CS1570;CS1574 + + + + + + + diff --git a/src/Elastic.Documentation/Search/IndexedProduct.cs b/src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs similarity index 100% rename from src/Elastic.Documentation/Search/IndexedProduct.cs rename to src/services/search/Elastic.Documentation.Search.Contract/IndexedProduct.cs diff --git a/src/services/Elastic.Documentation.Search/ChangesGateway.cs b/src/services/search/Elastic.Documentation.Search/ChangesService.cs similarity index 60% rename from src/services/Elastic.Documentation.Search/ChangesGateway.cs rename to src/services/search/Elastic.Documentation.Search/ChangesService.cs index d1715ba421..14e915ecb1 100644 --- a/src/services/Elastic.Documentation.Search/ChangesGateway.cs +++ b/src/services/search/Elastic.Documentation.Search/ChangesService.cs @@ -2,25 +2,57 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Buffers; +using System.Text; +using System.Text.Json; using Elastic.Clients.Elasticsearch; -using Elastic.Documentation.Api.Core.Changes; using Elastic.Documentation.Search.Common; using Microsoft.Extensions.Logging; namespace Elastic.Documentation.Search; /// -/// Elasticsearch gateway for the documentation changes feed. +/// Elasticsearch service for the documentation changes feed. /// Queries content_last_updated > since with search_after cursor pagination. /// Uses a shared Point In Time (PIT) for consistent pagination across requests. /// -public partial class ChangesGateway( +public partial class ChangesService( ElasticsearchClientAccessor clientAccessor, SharedPointInTimeManager pitManager, - ILogger logger -) : IChangesGateway + ILogger logger +) : IChangesService { - public async Task GetChangesAsync(ChangesRequest request, Cancel ctx = default) + public async Task GetChangesAsync(ChangesRequest request, Cancel ctx = default) + { + var cursor = DecodeCursor(request.Cursor); + var pageSize = Math.Clamp(request.PageSize, 1, ChangesDefaults.MaxPageSize); + + var internalRequest = new ChangesInternalRequest + { + Since = request.Since, + PageSize = pageSize, + Cursor = cursor + }; + + var result = await GetChangesInternalAsync(internalRequest, ctx); + + var nextCursor = result.NextCursor is not null + ? EncodeCursor(result.NextCursor) + : null; + + var hasMore = nextCursor is not null; + + LogChanges(logger, request.Since, result.Pages.Count, hasMore); + + return new ChangesResponse + { + Pages = result.Pages, + HasMore = hasMore, + NextCursor = nextCursor + }; + } + + private async Task GetChangesInternalAsync(ChangesInternalRequest request, Cancel ctx = default) { var fetchSize = request.PageSize + 1; @@ -57,7 +89,7 @@ public async Task GetChangesAsync(ChangesRequest request, Cancel } private async Task> Search( - ChangesRequest request, string pitId, int fetchSize, Cancel ctx + ChangesInternalRequest request, string pitId, int fetchSize, Cancel ctx ) => await clientAccessor.Client.SearchAsync(s => { @@ -147,6 +179,60 @@ private static ChangesResult BuildResult(SearchResponse r }; } + private static ChangesPageCursor? DecodeCursor(string? cursor) + { + if (string.IsNullOrWhiteSpace(cursor)) + return null; + + try + { + var remainder = cursor.Length % 4; + var paddingLength = (4 - remainder) % 4; + var base64 = cursor + .Replace('-', '+') + .Replace('_', '/') + + new string('=', paddingLength); + + var json = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + var arrayLength = root.GetArrayLength(); + if (root.ValueKind != JsonValueKind.Array || arrayLength < 2) + return null; + + var epochEl = root[0]; + var urlEl = root[1]; + if (epochEl.ValueKind != JsonValueKind.Number || urlEl.ValueKind != JsonValueKind.String) + return null; + + return new ChangesPageCursor(epochEl.GetInt64(), urlEl.GetString()!); + } + catch (Exception ex) when (ex is FormatException or JsonException or InvalidOperationException) + { + return null; + } + } + + private static string EncodeCursor(ChangesPageCursor cursor) + { + var buffer = new ArrayBufferWriter(); + using var writer = new Utf8JsonWriter(buffer); + writer.WriteStartArray(); + writer.WriteNumberValue(cursor.ContentLastUpdatedEpochMs); + writer.WriteStringValue(cursor.Url); + writer.WriteEndArray(); + writer.Flush(); + + return Convert.ToBase64String(buffer.WrittenSpan) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } + + [LoggerMessage(Level = LogLevel.Information, + Message = "Changes feed returned {Count} pages since {Since} (hasMore: {HasMore})")] + private static partial void LogChanges(ILogger logger, DateTimeOffset since, int count, bool hasMore); + [LoggerMessage(Level = LogLevel.Warning, Message = "PIT expired or not found, opening a new one and retrying with existing search_after position")] private static partial void LogPitExpired(ILogger logger); } diff --git a/src/services/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs rename to src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs diff --git a/src/services/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs b/src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs rename to src/services/search/Elastic.Documentation.Search/Common/SearchQueryBuilder.cs diff --git a/src/services/Elastic.Documentation.Search/Common/SearchResultProcessor.cs b/src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/Common/SearchResultProcessor.cs rename to src/services/search/Elastic.Documentation.Search/Common/SearchResultProcessor.cs diff --git a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj similarity index 66% rename from src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj rename to src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj index 8a350648d2..7287902825 100644 --- a/src/services/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj +++ b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj @@ -9,9 +9,8 @@ - - - + + diff --git a/src/services/Elastic.Documentation.Search/EsJsonContext.cs b/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/EsJsonContext.cs rename to src/services/search/Elastic.Documentation.Search/EsJsonContext.cs diff --git a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/FullSearchService.cs similarity index 91% rename from src/services/Elastic.Documentation.Search/FullSearchGateway.cs rename to src/services/search/Elastic.Documentation.Search/FullSearchService.cs index 54838c253f..7d78e4ec1e 100644 --- a/src/services/Elastic.Documentation.Search/FullSearchGateway.cs +++ b/src/services/search/Elastic.Documentation.Search/FullSearchService.cs @@ -5,7 +5,6 @@ using System.Text.RegularExpressions; using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch.QueryDsl; -using Elastic.Documentation.Api.Core.Search; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Search.Common; using Microsoft.Extensions.Logging; @@ -14,14 +13,14 @@ namespace Elastic.Documentation.Search; /// -/// Full-page search gateway implementation. +/// Full-page search service implementation. /// Uses hybrid RRF search for semantic queries, lexical-only for keyword queries. /// -public partial class FullSearchGateway( +public partial class FullSearchService( ElasticsearchClientAccessor clientAccessor, ProductsConfiguration productsConfiguration, - ILogger logger) - : IFullSearchGateway, IDisposable + ILogger logger) + : IFullSearchService, IDisposable { /// /// Regex pattern to detect semantic/question queries. @@ -61,17 +60,47 @@ private static bool IsSemanticQuery(string query) wordCount > 3; } - public async Task SearchAsync(FullSearchRequest request, Cancel ctx = default) + public async Task SearchAsync(FullSearchRequest request, Cancel ctx = default) { var isSemantic = IsSemanticQuery(request.Query); logger.LogDebug("Full search for query '{Query}' - semantic: {IsSemantic}", request.Query, isSemantic); - return isSemantic + var result = isSemantic ? await SearchWithHybridRrf(request, ctx) : await SearchLexicalOnly(request, ctx); + + var response = new FullSearchResponse + { + Results = result.Results, + TotalResults = result.TotalHits, + PageNumber = request.PageNumber, + PageSize = request.PageSize, + Aggregations = result.Aggregations, + IsSemanticQuery = result.IsSemanticQuery + }; + + LogFullSearchResults( + logger, + response.PageSize, + response.PageNumber, + request.Query, + result.IsSemanticQuery, + result.Results.Select(i => i.Url).ToArray() + ); + + return response; } + [LoggerMessage(Level = LogLevel.Information, Message = "Full search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}' (semantic: {IsSemantic}): {Urls}")] + private static partial void LogFullSearchResults( + ILogger logger, + int pageSize, + int pageNumber, + string searchQuery, + bool isSemantic, + string[] urls); + /// /// Performs hybrid RRF search combining lexical and semantic queries. /// Used when the query is detected as a semantic/question query. diff --git a/src/api/Elastic.Documentation.Api.Core/Changes/IChangesGateway.cs b/src/services/search/Elastic.Documentation.Search/IChangesService.cs similarity index 54% rename from src/api/Elastic.Documentation.Api.Core/Changes/IChangesGateway.cs rename to src/services/search/Elastic.Documentation.Search/IChangesService.cs index 27a02fe317..73c67703a0 100644 --- a/src/api/Elastic.Documentation.Api.Core/Changes/IChangesGateway.cs +++ b/src/services/search/Elastic.Documentation.Search/IChangesService.cs @@ -2,16 +2,32 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.Changes; +namespace Elastic.Documentation.Search; /// Gateway interface for querying documentation page changes. -public interface IChangesGateway +public interface IChangesService { - Task GetChangesAsync(ChangesRequest request, Cancel ctx = default); + Task GetChangesAsync(ChangesRequest request, Cancel ctx = default); } -/// Internal request for the changes gateway. +/// API request for the changes feed endpoint. public record ChangesRequest +{ + public required DateTimeOffset Since { get; init; } + public int PageSize { get; init; } = ChangesDefaults.PageSize; + public string? Cursor { get; init; } +} + +/// API response for the changes feed endpoint. +public record ChangesResponse +{ + public required IReadOnlyList Pages { get; init; } + public required bool HasMore { get; init; } + public string? NextCursor { get; init; } +} + +/// Internal request for the changes gateway. +public record ChangesInternalRequest { public required DateTimeOffset Since { get; init; } public int PageSize { get; init; } = ChangesDefaults.PageSize; @@ -34,3 +50,11 @@ public static class ChangesDefaults public const int PageSize = 100; public const int MaxPageSize = 1000; } + +/// A single changed page in the API response. +public record ChangedPageDto +{ + public required string Url { get; init; } + public required string Title { get; init; } + public required DateTimeOffset LastUpdated { get; init; } +} diff --git a/src/api/Elastic.Documentation.Api.Core/Search/IFullSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/IFullSearchService.cs similarity index 78% rename from src/api/Elastic.Documentation.Api.Core/Search/IFullSearchGateway.cs rename to src/services/search/Elastic.Documentation.Search/IFullSearchService.cs index 21ed3d6ce4..59af09bd69 100644 --- a/src/api/Elastic.Documentation.Api.Core/Search/IFullSearchGateway.cs +++ b/src/services/search/Elastic.Documentation.Search/IFullSearchService.cs @@ -2,15 +2,15 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -namespace Elastic.Documentation.Api.Core.Search; +namespace Elastic.Documentation.Search; /// -/// Gateway interface for full-page search operations. +/// Service interface for full-page search operations. /// Supports hybrid RRF search with semantic query detection. /// -public interface IFullSearchGateway +public interface IFullSearchService { - Task SearchAsync(FullSearchRequest request, Cancel ctx = default); + Task SearchAsync(FullSearchRequest request, Cancel ctx = default); } /// @@ -31,7 +31,23 @@ public record FullSearchRequest } /// -/// Result model for full-page search. +/// Response model for full-page search (unified API+service response). +/// +public record FullSearchResponse +{ + public required IReadOnlyList Results { get; init; } + public required int TotalResults { get; init; } + public required int PageNumber { get; init; } + public required int PageSize { get; init; } + public FullSearchAggregations Aggregations { get; init; } = new(); + public bool IsSemanticQuery { get; init; } + public int PageCount => TotalResults > 0 + ? (int)Math.Ceiling((double)TotalResults / PageSize) + : 0; +} + +/// +/// Result model for full-page search (used internally by the service implementation). /// public record FullSearchResult { diff --git a/src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs b/src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs new file mode 100644 index 0000000000..9fdcf6feb3 --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search/INavigationSearchService.cs @@ -0,0 +1,58 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Documentation.Search; + +public interface INavigationSearchService +{ + Task NavigationSearchAsync(NavigationSearchRequest request, Cancel ctx = default); +} + +public record NavigationSearchRequest +{ + public required string Query { get; init; } + public int PageNumber { get; init; } = 1; + public int PageSize { get; init; } = 20; + public string? TypeFilter { get; init; } +} + +public record NavigationSearchResponse +{ + public required IEnumerable Results { get; init; } + public required int TotalResults { get; init; } + public required int PageNumber { get; init; } + public required int PageSize { get; init; } + public NavigationSearchAggregations Aggregations { get; init; } = new(); + public int PageCount => TotalResults > 0 + ? (int)Math.Ceiling((double)TotalResults / PageSize) + : 0; +} + +public record NavigationSearchAggregations +{ + public IReadOnlyDictionary Type { get; init; } = new Dictionary(); +} + +public record NavigationSearchResult +{ + public required int TotalHits { get; init; } + public required List Results { get; init; } + public IReadOnlyDictionary Aggregations { get; init; } = new Dictionary(); +} + +public record NavigationSearchResultItemParent +{ + public required string Title { get; init; } + public required string Url { get; init; } +} + +public record NavigationSearchResultItem +{ + public required string Type { get; init; } + public required string Url { get; init; } + public required string Title { get; init; } + public required string Description { get; init; } + public required NavigationSearchResultItemParent[] Parents { get; init; } + public float Score { get; init; } +} diff --git a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/MockSearchService.cs similarity index 73% rename from src/services/Elastic.Documentation.Search/MockSearchGateway.cs rename to src/services/search/Elastic.Documentation.Search/MockSearchService.cs index 7cb8de28c3..a3dffb9672 100644 --- a/src/services/Elastic.Documentation.Search/MockSearchGateway.cs +++ b/src/services/search/Elastic.Documentation.Search/MockSearchService.cs @@ -2,11 +2,9 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Api.Core.Search; - namespace Elastic.Documentation.Search; -public class MockNavigationSearchGateway : INavigationSearchGateway +public class MockNavigationSearchService : INavigationSearchService { private static readonly List Results = [ @@ -82,39 +80,53 @@ public class MockNavigationSearchGateway : INavigationSearchGateway } ]; - public async Task NavigationSearchAsync(string query, int pageNumber, int pageSize, string? filter = null, CancellationToken ctx = default) + public async Task NavigationSearchAsync(NavigationSearchRequest request, CancellationToken ctx = default) { var filteredResults = Results .Where(item => - item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) || - item.Description?.Contains(query, StringComparison.OrdinalIgnoreCase) == true) + item.Title.Contains(request.Query, StringComparison.OrdinalIgnoreCase) || + item.Description?.Contains(request.Query, StringComparison.OrdinalIgnoreCase) == true) .ToList(); // Apply type filter if specified - if (!string.IsNullOrWhiteSpace(filter)) - filteredResults = filteredResults.Where(item => item.Type == filter).ToList(); + if (!string.IsNullOrWhiteSpace(request.TypeFilter)) + filteredResults = filteredResults.Where(item => item.Type == request.TypeFilter).ToList(); // Calculate aggregations before filtering var aggregations = Results .Where(item => - item.Title.Contains(query, StringComparison.OrdinalIgnoreCase) || - item.Description?.Contains(query, StringComparison.OrdinalIgnoreCase) == true) + item.Title.Contains(request.Query, StringComparison.OrdinalIgnoreCase) || + item.Description?.Contains(request.Query, StringComparison.OrdinalIgnoreCase) == true) .GroupBy(item => item.Type) .ToDictionary(g => g.Key, g => (long)g.Count()); var pagedResults = filteredResults - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize) + .Skip((request.PageNumber - 1) * request.PageSize) + .Take(request.PageSize) .ToList(); - Console.WriteLine($"MockSearchGateway: Paged results count: {pagedResults.Count}"); + Console.WriteLine($"MockSearchService: Paged results count: {pagedResults.Count}"); await Task.Delay(1000, ctx); - return new NavigationSearchResult + return new NavigationSearchResponse { - TotalHits = filteredResults.Count, + TotalResults = filteredResults.Count, Results = pagedResults, - Aggregations = aggregations + PageNumber = request.PageNumber, + PageSize = request.PageSize, + Aggregations = new NavigationSearchAggregations { Type = aggregations } }; } } + +public class MockFullSearchService : IFullSearchService +{ + public Task SearchAsync(FullSearchRequest request, Cancel ctx = default) => + Task.FromResult(new FullSearchResponse + { + Results = [], + TotalResults = 0, + PageNumber = request.PageNumber, + PageSize = request.PageSize + }); +} diff --git a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs b/src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs similarity index 87% rename from src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs rename to src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs index ec01a14947..49d3267a21 100644 --- a/src/services/Elastic.Documentation.Search/NavigationSearchGateway.cs +++ b/src/services/search/Elastic.Documentation.Search/NavigationSearchService.cs @@ -6,23 +6,46 @@ using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch.Core.Explain; using Elastic.Clients.Elasticsearch.QueryDsl; -using Elastic.Documentation.Api.Core.Search; using Elastic.Documentation.Search.Common; using Microsoft.Extensions.Logging; namespace Elastic.Documentation.Search; /// -/// Elasticsearch gateway for Navigation Search (autocomplete/navigation search). +/// Elasticsearch service for Navigation Search (autocomplete/navigation search). /// Uses shared lexical query optimized for autocomplete. /// -public class NavigationSearchGateway(ElasticsearchClientAccessor clientAccessor, ILogger logger) - : INavigationSearchGateway, IDisposable +public partial class NavigationSearchService(ElasticsearchClientAccessor clientAccessor, ILogger logger) + : INavigationSearchService, IDisposable { public async Task CanConnect(Cancel ctx) => await clientAccessor.CanConnect(ctx); - public async Task NavigationSearchAsync(string query, int pageNumber, int pageSize, string? filter = null, Cancel ctx = default) => - await SearchImplementation(query, pageNumber, pageSize, filter, ctx); + public async Task NavigationSearchAsync(NavigationSearchRequest request, Cancel ctx = default) + { + var result = await SearchImplementation(request.Query, request.PageNumber, request.PageSize, request.TypeFilter, ctx); + + var response = new NavigationSearchResponse + { + Results = result.Results, + TotalResults = result.TotalHits, + PageNumber = request.PageNumber, + PageSize = request.PageSize, + Aggregations = new NavigationSearchAggregations { Type = result.Aggregations } + }; + + LogNavigationSearchResults( + logger, + response.PageSize, + response.PageNumber, + request.Query, + result.Results.Select(i => i.Url).ToArray() + ); + + return response; + } + + [LoggerMessage(Level = LogLevel.Information, Message = "Navigation search completed with {PageSize} (page {PageNumber}) results for query '{SearchQuery}': {Urls}")] + private static partial void LogNavigationSearchResults(ILogger logger, int pageSize, int pageNumber, string searchQuery, string[] urls); public async Task SearchImplementation(string query, int pageNumber, int pageSize, string? filter = null, Cancel ctx = default) { diff --git a/src/services/Elastic.Documentation.Search/ServicesExtension.cs b/src/services/search/Elastic.Documentation.Search/ServicesExtension.cs similarity index 76% rename from src/services/Elastic.Documentation.Search/ServicesExtension.cs rename to src/services/search/Elastic.Documentation.Search/ServicesExtension.cs index 0571b470df..b6f92c4133 100644 --- a/src/services/Elastic.Documentation.Search/ServicesExtension.cs +++ b/src/services/search/Elastic.Documentation.Search/ServicesExtension.cs @@ -2,8 +2,6 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation.Api.Core.Changes; -using Elastic.Documentation.Api.Core.Search; using Elastic.Documentation.Search.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -29,18 +27,15 @@ public static IServiceCollection AddSearchServices(this IServiceCollection servi _ = services.AddSingleton(); // Navigation Search (autocomplete/navigation search) - _ = services.AddScoped(); - _ = services.AddScoped(); + _ = services.AddScoped(); // FullSearch (full-page search with hybrid RRF) - _ = services.AddScoped(); - _ = services.AddScoped(); - logger?.LogInformation("Full search use case registered with hybrid RRF support"); + _ = services.AddScoped(); + logger?.LogInformation("Full search services registered with hybrid RRF support"); // Changes feed (cursor-paginated changes since a given date) _ = services.AddSingleton(); - _ = services.AddScoped(); - _ = services.AddScoped(); + _ = services.AddScoped(); return services; } diff --git a/src/services/Elastic.Documentation.Search/SharedPointInTimeManager.cs b/src/services/search/Elastic.Documentation.Search/SharedPointInTimeManager.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/SharedPointInTimeManager.cs rename to src/services/search/Elastic.Documentation.Search/SharedPointInTimeManager.cs diff --git a/src/services/Elastic.Documentation.Search/StringHighlightExtensions.cs b/src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs similarity index 100% rename from src/services/Elastic.Documentation.Search/StringHighlightExtensions.cs rename to src/services/search/Elastic.Documentation.Search/StringHighlightExtensions.cs diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index 909c56823e..3c6a2c5f70 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -11,7 +11,6 @@ using Documentation.Builder.Arguments; using Elastic.Changelog; using Elastic.Changelog.Bundling; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Creation; using Elastic.Changelog.Evaluation; using Elastic.Changelog.GitHub; @@ -19,7 +18,9 @@ using Elastic.Changelog.Rendering; using Elastic.Changelog.Uploading; using Elastic.Changelog.Utilities; +using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; using Elastic.Documentation.Services; diff --git a/src/tooling/docs-builder/Http/DocumentationWebHost.cs b/src/tooling/docs-builder/Http/DocumentationWebHost.cs index 42c1c52499..b48d33b9a0 100644 --- a/src/tooling/docs-builder/Http/DocumentationWebHost.cs +++ b/src/tooling/docs-builder/Http/DocumentationWebHost.cs @@ -10,8 +10,9 @@ using System.Text.Json; using Documentation.Builder.Diagnostics.LiveMode; using Elastic.Documentation; +using Elastic.Documentation.Diagnostics; #if DEBUG -using Elastic.Documentation.Api.Infrastructure; +using Elastic.Documentation.Api; #endif using Elastic.Documentation.Configuration; using Elastic.Documentation.ServiceDefaults; @@ -34,7 +35,7 @@ public class DocumentationWebHost { private readonly WebApplication _webApplication; - private readonly IHostedService _hostedService; + private readonly IDiagnosticsCollector _hostedService; private readonly ScopedFileSystem _writeFileSystem; public InMemoryBuildState InMemoryBuildState { get; } @@ -53,7 +54,7 @@ bool isWatchBuild _ = builder.AddDocumentationServiceDefaults(); #if DEBUG - builder.Services.AddElasticDocsApiUsecases("dev"); + builder.Services.AddElasticDocsApiServices("dev"); #endif _ = builder.Logging diff --git a/src/tooling/docs-builder/Http/StaticWebHost.cs b/src/tooling/docs-builder/Http/StaticWebHost.cs index 788509e6bc..250d772184 100644 --- a/src/tooling/docs-builder/Http/StaticWebHost.cs +++ b/src/tooling/docs-builder/Http/StaticWebHost.cs @@ -4,7 +4,7 @@ using System.IO.Abstractions; #if DEBUG -using Elastic.Documentation.Api.Infrastructure; +using Elastic.Documentation.Api; #endif using Elastic.Documentation.Configuration; using Elastic.Documentation.Extensions; @@ -39,7 +39,7 @@ public StaticWebHost(int port, string? path) _ = builder.AddDocumentationServiceDefaults(); #if DEBUG - builder.Services.AddElasticDocsApiUsecases("dev"); + builder.Services.AddElasticDocsApiServices("dev"); #endif _ = builder.Logging diff --git a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs index 34f41953ff..35670eadd2 100644 --- a/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs +++ b/src/tooling/docs-builder/Middleware/CheckForUpdatesMiddleware.cs @@ -6,6 +6,7 @@ using System.Reflection; using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using Nullean.Argh.Middleware; diff --git a/src/tooling/docs-builder/docs-builder.csproj b/src/tooling/docs-builder/docs-builder.csproj index 40d37cb902..63cbb5997f 100644 --- a/src/tooling/docs-builder/docs-builder.csproj +++ b/src/tooling/docs-builder/docs-builder.csproj @@ -32,8 +32,7 @@ - - + diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj b/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj index a6be439513..e5b507a5fd 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj +++ b/tests-integration/Elastic.Assembler.IntegrationTests/Elastic.Assembler.IntegrationTests.csproj @@ -11,8 +11,7 @@ - - + diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs index 8bf29348cf..4f351d3927 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/Search/SearchIntegrationTests.cs @@ -4,7 +4,7 @@ using System.Net.Http.Json; using AwesomeAssertions; -using Elastic.Documentation.Api.Core.Search; +using Elastic.Documentation.Search; namespace Elastic.Assembler.IntegrationTests.Search; @@ -55,18 +55,18 @@ public async Task SearchEndpointReturnsExpectedFirstResult(string query, string // Assert - Response should be successful response.EnsureSuccessStatusCode(); - var searchResponse = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); + var searchResponse = await response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); searchResponse.Should().NotBeNull("Search response should be deserialized"); // Log results for debugging output.WriteLine($"Query: {query}"); output.WriteLine($"Total results: {searchResponse.TotalResults}"); - output.WriteLine($"Results returned: {searchResponse.Results.Count()}"); + output.WriteLine($"Results returned: {searchResponse.Results.Count}"); if (searchResponse.Results.Any()) { output.WriteLine("First result:"); - var firstResult = searchResponse.Results.First(); + var firstResult = searchResponse.Results[0]; output.WriteLine($" Title: {firstResult.Title}"); output.WriteLine($" URL: {firstResult.Url}"); output.WriteLine($" Score: {firstResult.Score}"); @@ -76,7 +76,7 @@ public async Task SearchEndpointReturnsExpectedFirstResult(string query, string searchResponse.Results.Should().NotBeEmpty($"Search for '{query}' should return results"); // Assert - First result should match expected URL - var actualFirstResultUrl = searchResponse.Results.First().Url; + var actualFirstResultUrl = searchResponse.Results[0].Url; actualFirstResultUrl.Should().Be(expectedFirstResultUrl, $"First result for query '{query}' should be the expected documentation page"); } @@ -93,12 +93,12 @@ public async Task SearchEndpointWithPaginationReturnsCorrectPage() // Act - Get first page var page1Response = await searchFixture.HttpClient.GetAsync($"/docs/_api/v1/search?q={Uri.EscapeDataString(query)}&page=1", TestContext.Current.CancellationToken); page1Response.EnsureSuccessStatusCode(); - var page1Data = await page1Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); + var page1Data = await page1Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); // Act - Get second page var page2Response = await searchFixture.HttpClient.GetAsync($"/docs/_api/v1/search?q={Uri.EscapeDataString(query)}&page=2", TestContext.Current.CancellationToken); page2Response.EnsureSuccessStatusCode(); - var page2Data = await page2Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); + var page2Data = await page2Response.Content.ReadFromJsonAsync(cancellationToken: TestContext.Current.CancellationToken); // Assert page1Data.Should().NotBeNull(); diff --git a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs index 95a250c570..01bb997ef0 100644 --- a/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs +++ b/tests-integration/Elastic.Assembler.IntegrationTests/TestHelpers.cs @@ -10,6 +10,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.Assembler.IntegrationTests; diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs index aa8e7c9887..2b2fc35712 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/AskAiGatewayStreamingTests.cs @@ -5,9 +5,9 @@ using System.Net; using System.Text; using AwesomeAssertions; -using Elastic.Documentation.Api.Core.AskAi; -using Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; -using Elastic.Documentation.Api.Infrastructure.Gcp; +using Elastic.Documentation.Api.Adapters.AskAi; +using Elastic.Documentation.Api.AskAi; +using Elastic.Documentation.Api.Gcp; using FakeItEasy; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj index 569011a7f2..3e30d7dc32 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Elastic.Documentation.Api.IntegrationTests.csproj @@ -5,9 +5,7 @@ - - - + diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs index e612802042..c555951748 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/EuidEnrichmentIntegrationTests.cs @@ -5,8 +5,8 @@ using System.Diagnostics; using System.Text; using AwesomeAssertions; -using Elastic.Documentation.Api.Core; -using Elastic.Documentation.Api.Core.AskAi; +using Elastic.Documentation.Api; +using Elastic.Documentation.Api.AskAi; using Elastic.Documentation.Api.IntegrationTests.Fixtures; using FakeItEasy; using Microsoft.Extensions.DependencyInjection; @@ -50,8 +50,8 @@ public async Task AskAiEndpointPropagatatesEuidToAllSpansAndLogs() // Create factory with mocked AskAi services using var factory = ApiWebApplicationFactory.WithMockedServices(services => { - // Mock IAskAiGateway to avoid external AI service calls - var mockAskAiGateway = A.Fake(); + // Mock IAskAiService to avoid external AI service calls + var mockAskAiGateway = A.Fake(); A.CallTo(() => mockAskAiGateway.AskAi(A._, A._)) .ReturnsLazily(() => { @@ -113,11 +113,10 @@ public async Task AskAiEndpointPropagatatesEuidToAllSpansAndLogs() var logRecords = factory.ExportedLogRecords; logRecords.Should().NotBeEmpty("Should have captured log records"); - // Find a log entry from AskAiUsecase + // Find a log entry from the AskAI endpoint handler var askAiLogRecord = logRecords.FirstOrDefault(r => - string.Equals(r.CategoryName, typeof(AskAiUsecase).FullName, StringComparison.OrdinalIgnoreCase) && r.FormattedMessage?.Contains("Starting AskAI", StringComparison.OrdinalIgnoreCase) == true); - askAiLogRecord.Should().NotBeNull("Should have logged from AskAiUsecase"); + askAiLogRecord.Should().NotBeNull("Should have logged from AskAI endpoint handler"); // Verify euid is present in OTEL log attributes (mirrors production exporter behavior) var euidAttribute = askAiLogRecord.Attributes?.FirstOrDefault(a => a.Key == TelemetryConstants.UserEuidAttributeName) ?? default; diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs index 47794d8a77..d7d5bf88ed 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/Fixtures/ApiWebApplicationFactory.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information using System.Diagnostics; -using Elastic.Documentation.Api.Infrastructure; -using Elastic.Documentation.Api.Infrastructure.Aws; -using Elastic.Documentation.Api.Infrastructure.OpenTelemetry; +using Elastic.Documentation.Api; +using Elastic.Documentation.Api.Aws; +using Elastic.Documentation.Api.OpenTelemetry; using FakeItEasy; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; diff --git a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs index 8f5c48dff9..99db7df7da 100644 --- a/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs +++ b/tests-integration/Elastic.Documentation.Api.IntegrationTests/OtlpProxyIntegrationTests.cs @@ -5,8 +5,8 @@ using System.Net; using System.Text; using AwesomeAssertions; -using Elastic.Documentation.Api.Infrastructure.Adapters.Telemetry; using Elastic.Documentation.Api.IntegrationTests.Fixtures; +using Elastic.Documentation.Api.Telemetry; using FakeItEasy; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -52,7 +52,7 @@ public async Task OtlpProxyTracesEndpointForwardsToCorrectUrl() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { // Replace the named HttpClient with our mock - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler); }); @@ -116,7 +116,7 @@ public async Task OtlpProxyLogsEndpointForwardsToCorrectUrl() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler); }); @@ -173,7 +173,7 @@ public async Task OtlpProxyMetricsEndpointForwardsToCorrectUrl() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler); }); @@ -225,7 +225,7 @@ public async Task OtlpProxyReturnsCollectorErrorStatusCode() using var factory = ApiWebApplicationFactory.WithMockedServices(services => { #pragma warning disable EXTEXP0001 // Experimental API - needed for test to bypass resilience handlers - _ = services.AddHttpClient(AdotOtlpGateway.HttpClientName) + _ = services.AddHttpClient(AdotOtlpService.HttpClientName) .ConfigurePrimaryHttpMessageHandler(() => mockHandler) .RemoveAllResilienceHandlers(); #pragma warning restore EXTEXP0001 diff --git a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj index 8889bbcbaa..0b21ba1622 100644 --- a/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj +++ b/tests-integration/Mcp.Remote.IntegrationTests/Mcp.Remote.IntegrationTests.csproj @@ -7,9 +7,8 @@ - - + diff --git a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs index 0d231b762a..7c00de5d58 100644 --- a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs +++ b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information using System.Collections.Frozen; -using Elastic.Documentation.Api.Core.Search; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Mcp.Remote.Gateways; @@ -48,10 +47,10 @@ protected async Task LogIndexCount(ElasticsearchClientAccessor clientAccessor, C var clientAccessor = CreateElasticsearchClientAccessor(); var productsConfig = CreateProductsConfiguration(); - var fullSearchGateway = new FullSearchGateway( + var fullSearchGateway = new FullSearchService( clientAccessor, productsConfig, - NullLogger.Instance + NullLogger.Instance ); var searchTools = new SearchTools(fullSearchGateway, NullLogger.Instance); @@ -78,7 +77,7 @@ protected async Task LogIndexCount(ElasticsearchClientAccessor clientAccessor, C var clientAccessor = CreateElasticsearchClientAccessor(); var productsConfig = CreateProductsConfiguration(); - var fullSearchGateway = new FullSearchGateway(clientAccessor, productsConfig, NullLogger.Instance); + var fullSearchGateway = new FullSearchService(clientAccessor, productsConfig, NullLogger.Instance); var coherenceTools = new CoherenceTools(fullSearchGateway, NullLogger.Instance); return (coherenceTools, clientAccessor); } diff --git a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj index 4763c5eb0d..e2367742fd 100644 --- a/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj +++ b/tests-integration/Search.IntegrationTests/Search.IntegrationTests.csproj @@ -7,9 +7,8 @@ - - - + + diff --git a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs index 4f25592bf8..060a75e7ff 100644 --- a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs +++ b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs @@ -239,14 +239,14 @@ public async Task ExplainTopResultAndExpectedAsyncReturnsDetailedScoring() /// /// Creates an ElasticsearchGateway instance using configuration from the distributed application. /// - private static (NavigationSearchGateway Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway() + private static (NavigationSearchService Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway() { var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler", environment: "dev"); var configProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new FileSystem(), configurationSource: ConfigurationSource.Embedded); var searchConfig = configProvider.CreateSearchConfiguration(); var clientAccessor = new ElasticsearchClientAccessor(endpoints, searchConfig); - var gateway = new NavigationSearchGateway(clientAccessor, NullLogger.Instance); + var gateway = new NavigationSearchService(clientAccessor, NullLogger.Instance); return (gateway, clientAccessor); } } diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs index a2ed6c78ae..86433fd286 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiDocumentExporterTests.cs @@ -8,6 +8,7 @@ using Elastic.Documentation; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Search; +using Elastic.Documentation.Versions; using static System.StringComparison; namespace Elastic.ApiExplorer.Tests; diff --git a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs index eed99c1bcb..26727feb30 100644 --- a/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs +++ b/tests/Elastic.ApiExplorer.Tests/TestHelpers.cs @@ -12,6 +12,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.ApiExplorer.Tests; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs index 712b379e35..3bfe1528c2 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogConfigurationTests.cs @@ -3,11 +3,11 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; -using Elastic.Changelog.Configuration; -using Elastic.Changelog.Serialization; using Elastic.Documentation; using Elastic.Documentation.Configuration; using Elastic.Documentation.Configuration.Changelog; +using Elastic.Documentation.Configuration.Changelog; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs index 5e3b93a57d..832d20ab2c 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/ChangelogTestBase.cs @@ -11,6 +11,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Nullean.ScopedFileSystem; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs index e00e7ddb75..1c485eedfb 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/CreateChangelogTestBase.cs @@ -4,6 +4,7 @@ using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs index 0f031aacb7..900013ac71 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/ReleaseNoteExtractionTests.cs @@ -5,6 +5,7 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; using Elastic.Changelog.GitHub; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs index bd72bf9c67..03f0c54020 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs @@ -4,9 +4,9 @@ using AwesomeAssertions; using Elastic.Changelog.Bundling; -using Elastic.Changelog.Configuration; using Elastic.Changelog.Rendering; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Changelog; using Elastic.Documentation.Diagnostics; namespace Elastic.Changelog.Tests.Changelogs.Render; diff --git a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs index bba674d2c4..ab38e30eb0 100644 --- a/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs +++ b/tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs @@ -5,6 +5,7 @@ using AwesomeAssertions; using Elastic.Changelog.Creation; using Elastic.Changelog.Tests.Changelogs; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs index b4f7e4fc30..60480bbc4f 100644 --- a/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs +++ b/tests/Elastic.Changelog.Tests/Creation/ChangelogCreationServiceTests.cs @@ -9,6 +9,7 @@ using Elastic.Changelog.GitHub; using Elastic.Changelog.Tests.Changelogs; using Elastic.Changelog.Utilities; +using Elastic.Documentation; using Elastic.Documentation.Configuration; using FakeItEasy; diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs index 8cbbefa7d7..111d5bc9a9 100644 --- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs +++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Adapters/AskAi/StreamTransformerTests.cs @@ -6,8 +6,8 @@ using System.Text; using System.Text.Json; using AwesomeAssertions; -using Elastic.Documentation.Api.Core.AskAi; -using Elastic.Documentation.Api.Infrastructure.Adapters.AskAi; +using Elastic.Documentation.Api.Adapters.AskAi; +using Elastic.Documentation.Api.AskAi; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.Documentation.Api.Infrastructure.Tests.Adapters.AskAi; diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs index a651cd619a..b487c60d36 100644 --- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs +++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Caching/DistributedCacheTests.cs @@ -7,8 +7,8 @@ using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using AwesomeAssertions; -using Elastic.Documentation.Api.Infrastructure.Caching; -using Elastic.Documentation.Api.Infrastructure.Gcp; +using Elastic.Documentation.Api.Caching; +using Elastic.Documentation.Api.Gcp; using FakeItEasy; using Microsoft.Extensions.Logging.Abstractions; diff --git a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj index 2d317e4f79..67afbae2e4 100644 --- a/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj +++ b/tests/Elastic.Documentation.Api.Infrastructure.Tests/Elastic.Documentation.Api.Infrastructure.Tests.csproj @@ -5,9 +5,8 @@ - - - + + diff --git a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs index 45cb7deaee..284b950c4a 100644 --- a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs +++ b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using Elastic.Documentation; using Elastic.Documentation.Configuration; namespace Elastic.Documentation.Build.Tests; diff --git a/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs b/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs index e165b09555..87bc8daec9 100644 --- a/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs +++ b/tests/Elastic.Documentation.Build.Tests/SymlinkValidationTests.cs @@ -5,6 +5,7 @@ using System.IO.Abstractions.TestingHelpers; using System.Security; using AwesomeAssertions; +using Elastic.Documentation; using Elastic.Documentation.Configuration; namespace Elastic.Documentation.Build.Tests; diff --git a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs index 51b84cd905..804502ebca 100644 --- a/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs +++ b/tests/Elastic.Documentation.Build.Tests/TestHelpers.cs @@ -15,6 +15,7 @@ using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.Versions; using FakeItEasy; using Microsoft.Extensions.Logging; diff --git a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs index f9c9a6ddff..7f27e0838a 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ApiConfigurationTests.cs @@ -458,6 +458,6 @@ private sealed class MockDocumentationSetContext( public IFileInfo ConfigurationPath => configurationPath; public BuildType BuildType => BuildType.Isolated; public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory; - public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem); + public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem); } } diff --git a/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs b/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs index 35a6845c14..fbece05e0c 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/AssemblyConfigurationMatchTests.cs @@ -6,6 +6,7 @@ using Elastic.Documentation.Configuration.Assembler; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using MatchResult = Elastic.Documentation.Configuration.Assembler.AssemblyConfiguration.ContentSourceMatch; diff --git a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs index 6c31693f72..1cb21c1e3e 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ConfigurationFileExcludeTests.cs @@ -93,6 +93,6 @@ private sealed class MockDocumentationSetContext( public IFileInfo ConfigurationPath => configurationPath; public BuildType BuildType => BuildType.Isolated; public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory; - public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem); + public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem); } } diff --git a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs index ed224f2ab8..5e34366c80 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/CrossLinkRegistryTests.cs @@ -139,6 +139,6 @@ private sealed class MockDocumentationSetContext( public IFileInfo ConfigurationPath => configurationPath; public BuildType BuildType => BuildType.Isolated; public IDirectoryInfo DocumentationSourceDirectory => documentationSourceDirectory; - public GitCheckoutInformation Git => GitCheckoutInformation.Create(documentationSourceDirectory, fileSystem); + public GitCheckoutInformation Git => GitCheckoutInformationFactory.Create(documentationSourceDirectory, fileSystem); } } diff --git a/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs index 00a5796e9b..dd4268bb79 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/DocumentInferrerServiceTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Configuration.LegacyUrlMappings; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Configuration.Tests; diff --git a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj index 0dc80a953d..fc32e597e0 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj +++ b/tests/Elastic.Documentation.Configuration.Tests/Elastic.Documentation.Configuration.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs index 4d6a17173d..172c628b19 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/VersionOrDateTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using AwesomeAssertions; +using Elastic.Documentation.Versions; namespace Elastic.Documentation.Configuration.Tests.ReleaseNotes; diff --git a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs index 0198135682..4ea6c22fd2 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/VersionInferenceTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.Configuration.Inference; using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; using Microsoft.Extensions.Logging.Abstractions; namespace Elastic.Documentation.Configuration.Tests; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs index 66aaf303e6..d71b39d32d 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs @@ -6,6 +6,7 @@ using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.AppliesTo; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs index fd5ab13d01..376f7748ba 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs @@ -7,6 +7,7 @@ using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.AppliesTo; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs index c8700f00c6..34dbf86941 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs @@ -3,10 +3,11 @@ // See the LICENSE file in the project root for more information using System.Reflection; +using System.Text.Json.Serialization; using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; -using YamlDotNet.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.AppliesTo; @@ -19,7 +20,7 @@ public void ProductApplicabilityToStringIncludesAllProperties() var productApplicability = new ProductApplicability(); var productType = typeof(ProductApplicability); var properties = productType.GetProperties() - .Where(p => p.GetCustomAttribute() != null) + .Where(p => p.GetCustomAttribute() != null) .ToList(); // Set all properties to a test value @@ -35,9 +36,9 @@ public void ProductApplicabilityToStringIncludesAllProperties() // Verify that each property's YAML alias appears in the output foreach (var property in properties) { - var yamlAlias = property.GetCustomAttribute()!.Alias; - result.Should().Contain($"{yamlAlias}=", - $"ToString should include the property {property.Name} with alias '{yamlAlias}'"); + var jsonName = property.GetCustomAttribute()!.Name; + result.Should().Contain($"{jsonName}=", + $"ToString should include the property {property.Name} with alias '{jsonName}'"); } // Verify we have the expected number of properties @@ -90,8 +91,8 @@ public void ProductApplicabilityToStringPropertyOrderMatchesReflectionOrder() // Get the properties in reflection order var productType = typeof(ProductApplicability); var properties = productType.GetProperties() - .Where(p => p.GetCustomAttribute() != null) - .Select(p => p.GetCustomAttribute()!.Alias) + .Where(p => p.GetCustomAttribute() != null) + .Select(p => p.GetCustomAttribute()!.Name) .ToList(); // Find positions in the string diff --git a/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs b/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs index 8072ebba37..74185b46ce 100644 --- a/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs +++ b/tests/Elastic.Markdown.Tests/CrossLinks/UriEnvironmentResolverTests.cs @@ -6,6 +6,7 @@ using AwesomeAssertions; using Elastic.Documentation; using Elastic.Documentation.Configuration; +using Elastic.Documentation.Configuration.Builder; using Elastic.Documentation.Links; using Elastic.Documentation.Links.CrossLinks; diff --git a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs index 88f247d8bd..4b41e3e982 100644 --- a/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/VersionTests.cs @@ -4,6 +4,7 @@ using AwesomeAssertions; using Elastic.Documentation; +using Elastic.Documentation.Versions; using Elastic.Markdown.Myst.Directives.Version; namespace Elastic.Markdown.Tests.Directives; diff --git a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs index f09101d480..21ccbf2707 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/RepositoryLinksTests.cs @@ -35,7 +35,7 @@ public class GitCheckoutInformationTests(ITestOutputHelper output) : NavigationT public void Create() { var root = ReadFileSystem.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName); - var git = GitCheckoutInformation.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation))); + var git = GitCheckoutInformationFactory.Create(root, ReadFileSystem, LoggerFactory.CreateLogger(nameof(GitCheckoutInformation))); git.Should().NotBeNull(); git.Branch.Should().NotBeNullOrWhiteSpace(); diff --git a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj index a2a90aa4d3..5d1439526f 100644 --- a/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj +++ b/tests/Elastic.Markdown.Tests/Elastic.Markdown.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs index b85bc26db0..8381feb999 100644 --- a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs @@ -9,6 +9,7 @@ using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Search; using Elastic.Documentation.Serialization; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests.Search; @@ -28,7 +29,7 @@ public void SerializeDocumentWithStackAppliesToProducesCorrectJson() Applies = new ApplicableTo { Stack = AppliesCollection.GenerallyAvailable - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -66,7 +67,7 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson() Ess = AppliesCollection.GenerallyAvailable, Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"3.5.0" }]) } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -108,7 +109,7 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson() Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }]), Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"1.0.0" }]) } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -146,7 +147,7 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson() Applies = new ApplicableTo { Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]) - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -180,7 +181,7 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson() ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.5.0" }]), ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"2.0.0" }]) } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -226,7 +227,7 @@ public void SerializeDocumentWithComplexAppliesToProducesCorrectJson() { Elasticsearch = AppliesCollection.GenerallyAvailable } - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -279,7 +280,7 @@ public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray() Url = "/test/empty-applies", Title = "Empty Applies Test", SearchTitle = "Empty Applies Test", - Applies = new ApplicableTo() + Applies = new ApplicableTo().ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); @@ -294,6 +295,15 @@ public void SerializeDocumentWithEmptyAppliesToProducesEmptyArray() [Fact] public void RoundTripDocumentWithAppliesToPreservesData() { + var originalApplies = new ApplicableTo + { + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]), + Deployment = new DeploymentApplicability + { + Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }]) + } + }.ToAppliesTo(); + var original = new DocumentationDocument { Type = "doc", @@ -305,14 +315,7 @@ public void RoundTripDocumentWithAppliesToPreservesData() LastUpdated = DateTimeOffset.Parse("2024-01-15T09:00:00Z", CultureInfo.InvariantCulture), ContentLastUpdated = DateTimeOffset.Parse("2024-01-14T08:00:00Z", CultureInfo.InvariantCulture), ContentBodyHash = "abc123def456abc1", - Applies = new ApplicableTo - { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]), - Deployment = new DeploymentApplicability - { - Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }]) - } - }, + Applies = originalApplies, Headings = ["Introduction", "Getting Started"], Links = ["/link1", "/link2"], Body = "Test body content", @@ -327,9 +330,9 @@ public void RoundTripDocumentWithAppliesToPreservesData() deserialized.Url.Should().Be(original.Url); deserialized.Title.Should().Be(original.Title); deserialized.Applies.Should().NotBeNull(); - deserialized.Applies.Stack.Should().BeEquivalentTo(original.Applies.Stack); - deserialized.Applies.Deployment.Should().NotBeNull(); - deserialized.Applies.Deployment.Ess.Should().BeEquivalentTo(original.Applies.Deployment.Ess); + deserialized.Applies.Should().HaveCount(2); + deserialized.Applies.Should().Contain(e => e.Type == "stack" && e.Lifecycle == "ga" && e.Version == "8.5+"); + deserialized.Applies.Should().Contain(e => e.Type == "deployment" && e.SubType == "ess" && e.Lifecycle == "beta" && e.Version == "8.6+"); deserialized.ContentLastUpdated.Should().Be(original.ContentLastUpdated); deserialized.ContentBodyHash.Should().Be(original.ContentBodyHash); deserialized.ContentType.Should().Be(original.ContentType); @@ -395,7 +398,7 @@ public void SerializeDocumentWithMultipleApplicabilitiesPerTypeProducesMultipleA new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" }, new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"7.0.0" } ]) - } + }.ToAppliesTo() }; var json = JsonSerializer.Serialize(doc, _options); diff --git a/tests/Elastic.Markdown.Tests/TestHelpers.cs b/tests/Elastic.Markdown.Tests/TestHelpers.cs index e8d9aac96a..2aa37abeab 100644 --- a/tests/Elastic.Markdown.Tests/TestHelpers.cs +++ b/tests/Elastic.Markdown.Tests/TestHelpers.cs @@ -10,6 +10,7 @@ using Elastic.Documentation.Configuration.Products; using Elastic.Documentation.Configuration.Search; using Elastic.Documentation.Configuration.Versions; +using Elastic.Documentation.Versions; namespace Elastic.Markdown.Tests; diff --git a/tests/authoring/Framework/Setup.fs b/tests/authoring/Framework/Setup.fs index d89f426d12..9f79710fca 100644 --- a/tests/authoring/Framework/Setup.fs +++ b/tests/authoring/Framework/Setup.fs @@ -13,6 +13,7 @@ open System.IO.Abstractions.TestingHelpers open System.Threading.Tasks open YamlDotNet.RepresentationModel open Elastic.Documentation +open Elastic.Documentation.Versions open Elastic.Documentation.Configuration open Elastic.Documentation.Configuration.LegacyUrlMappings open Elastic.Documentation.Configuration.Versions