From f3c28d56904cdb6a54f84324507e8902010d6502 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 3 Jun 2026 10:53:43 +0200 Subject: [PATCH] fix: remove RuleQueryMatchCriteria hacks and prefer env vars over secrets - Bump Elastic.Internal.Search.{Contract,Elasticsearch} to 0.9.2 which exposes RuleQueryMatchCriteria and its SourceGenerationContext for AOT-compatible serialization - Remove the reflection-based RuleQueryMatchCriteriaTypeInfoResolver and RuleQueryMatchCriteriaJsonConverterFactory that broke under .NET 10's stricter JsonSerializerOptions compatibility checks - Chain the new Elasticsearch package SourceGenerationContext into the resolver so rule queries serialize correctly without reflection - Reorder config lookups in ElasticsearchEndpointFactory so environment variables take precedence over dotnet user secrets - Suppress noisy OpenTelemetry logs (default to Warning) in MCP server appsettings Co-authored-by: Cursor --- Directory.Packages.props | 4 +- .../ElasticsearchEndpointFactory.cs | 27 ++++--- .../appsettings.development.json | 3 +- .../appsettings.edge.json | 3 +- .../appsettings.json | 3 +- .../Common/ElasticsearchClientAccessor.cs | 5 +- .../Common/ElasticsearchClientJsonResolver.cs | 12 ++- .../RuleQueryMatchCriteriaJsonConverter.cs | 76 ------------------- .../RuleQueryMatchCriteriaTypeInfoResolver.cs | 46 ----------- .../McpToolsIntegrationTestsBase.cs | 6 +- .../SearchRelevanceTests.cs | 2 +- 11 files changed, 35 insertions(+), 152 deletions(-) delete mode 100644 src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs delete mode 100644 src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 8d280c875a..a00ac8f6df 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -51,8 +51,8 @@ - - + + diff --git a/src/Elastic.Documentation.ServiceDefaults/ElasticsearchEndpointFactory.cs b/src/Elastic.Documentation.ServiceDefaults/ElasticsearchEndpointFactory.cs index 6d1bd7ba5c..c09c244193 100644 --- a/src/Elastic.Documentation.ServiceDefaults/ElasticsearchEndpointFactory.cs +++ b/src/Elastic.Documentation.ServiceDefaults/ElasticsearchEndpointFactory.cs @@ -7,13 +7,14 @@ namespace Elastic.Documentation.ServiceDefaults; -/// Centralizes user-secrets + env-var reading for Elasticsearch configuration. +/// Centralizes env-var + user-secrets reading for Elasticsearch configuration. public static class ElasticsearchEndpointFactory { private const string UserSecretsId = "72f50f33-6fb9-4d08-bff3-39568fe370b3"; /// /// Creates from user secrets and environment variables. + /// Environment variables take priority over user secrets. /// public static DocumentationEndpoints Create(IConfiguration? appConfiguration = null, string? buildType = null, string? environment = null) { @@ -23,20 +24,20 @@ public static DocumentationEndpoints Create(IConfiguration? appConfiguration = n var config = configBuilder.Build(); var url = - config["Parameters:DocumentationElasticUrl"] - ?? config["DOCUMENTATION_ELASTIC_URL"]; + config["DOCUMENTATION_ELASTIC_URL"] + ?? config["Parameters:DocumentationElasticUrl"]; var apiKey = - config["Parameters:DocumentationElasticApiKey"] - ?? config["DOCUMENTATION_ELASTIC_APIKEY"]; + config["DOCUMENTATION_ELASTIC_APIKEY"] + ?? config["Parameters:DocumentationElasticApiKey"]; var password = - config["Parameters:DocumentationElasticPassword"] - ?? config["DOCUMENTATION_ELASTIC_PASSWORD"]; + config["DOCUMENTATION_ELASTIC_PASSWORD"] + ?? config["Parameters:DocumentationElasticPassword"]; var username = - config["Parameters:DocumentationElasticUsername"] - ?? config["DOCUMENTATION_ELASTIC_USERNAME"] + config["DOCUMENTATION_ELASTIC_USERNAME"] + ?? config["Parameters:DocumentationElasticUsername"] ?? "elastic"; if (string.IsNullOrEmpty(url)) @@ -59,8 +60,8 @@ public static DocumentationEndpoints Create(IConfiguration? appConfiguration = n buildType ??= appConfiguration?["DOCS_BUILD_TYPE"] ?? config["DOCS_BUILD_TYPE"] ?? "isolated"; var searchIndexOverride = - config["Parameters:DocumentationElasticIndexOverride"] - ?? config["DOCUMENTATION_ELASTIC_INDEX_OVERRIDE"]; + config["DOCUMENTATION_ELASTIC_INDEX_OVERRIDE"] + ?? config["Parameters:DocumentationElasticIndexOverride"]; return new DocumentationEndpoints { @@ -84,6 +85,10 @@ private static string ResolveEnvironment(IConfiguration config, IConfiguration? ?? config["DOTNET_ENVIRONMENT"] ?? config["ENVIRONMENT"]; + string[] allowedEnvironements = ["dev", "prod", "staging"]; + if (!allowedEnvironements.Contains(envVar)) + envVar = "dev"; + return !string.IsNullOrEmpty(envVar) ? envVar.ToLowerInvariant() : "dev"; } } diff --git a/src/api/Elastic.Documentation.Mcp.Remote/appsettings.development.json b/src/api/Elastic.Documentation.Mcp.Remote/appsettings.development.json index 34f00ef136..148200bc04 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/appsettings.development.json +++ b/src/api/Elastic.Documentation.Mcp.Remote/appsettings.development.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Debug", - "Microsoft.AspNetCore": "Information" + "Microsoft.AspNetCore": "Information", + "OpenTelemetry": "Warning" } } } diff --git a/src/api/Elastic.Documentation.Mcp.Remote/appsettings.edge.json b/src/api/Elastic.Documentation.Mcp.Remote/appsettings.edge.json index 0c208ae918..4afa71e152 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/appsettings.edge.json +++ b/src/api/Elastic.Documentation.Mcp.Remote/appsettings.edge.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "OpenTelemetry": "Warning" } } } diff --git a/src/api/Elastic.Documentation.Mcp.Remote/appsettings.json b/src/api/Elastic.Documentation.Mcp.Remote/appsettings.json index 10f68b8c8b..9e7dcb3c2e 100644 --- a/src/api/Elastic.Documentation.Mcp.Remote/appsettings.json +++ b/src/api/Elastic.Documentation.Mcp.Remote/appsettings.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "OpenTelemetry": "Warning" } }, "AllowedHosts": "*" diff --git a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs index b06d06166d..35b3829d30 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs @@ -68,8 +68,9 @@ SearchConfiguration searchConfiguration _nodePool, sourceSerializer: (_, settings) => new DefaultSourceSerializer( settings, - ElasticsearchClientJsonResolver.Default, - static jsonOptions => jsonOptions.Converters.Add(RuleQueryMatchCriteriaJsonConverterFactory.Instance))) + ElasticsearchClientJsonResolver.Default + ) + ) .DefaultIndex(SearchIndex) .Authentication(auth); diff --git a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs index 95fb70d1c0..184371dbb9 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs @@ -16,12 +16,10 @@ internal static class ElasticsearchClientJsonResolver { public static IJsonTypeInfoResolver Default { get; } = Create(); - private static IJsonTypeInfoResolver Create() - { - var combined = JsonTypeInfoResolver.Combine( + private static IJsonTypeInfoResolver Create() => + JsonTypeInfoResolver.Combine( InternalSearch.SourceGenerationContext.Default, - SourceGenerationContext.Default); - - return new RuleQueryMatchCriteriaTypeInfoResolver(combined); - } + InternalSearch.Elasticsearch.SourceGenerationContext.Default, + SourceGenerationContext.Default + ); } diff --git a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs deleted file mode 100644 index eb34abae94..0000000000 --- a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs +++ /dev/null @@ -1,76 +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.CodeAnalysis; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Elastic.Documentation.Search.Common; - -/// -/// Serializes Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, which is internal to the -/// Elasticsearch search package and therefore not covered by public types. -/// -internal sealed class RuleQueryMatchCriteriaJsonConverterFactory : JsonConverterFactory -{ - public static readonly RuleQueryMatchCriteriaJsonConverterFactory Instance = new(); - - [DynamicDependency( - DynamicallyAccessedMemberTypes.All, - "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria", - "Elastic.Internal.Search.Elasticsearch")] - private static readonly Type RuleQueryMatchCriteriaType = Type.GetType( - "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, Elastic.Internal.Search.Elasticsearch", - throwOnError: true)!; - - public override bool CanConvert(Type typeToConvert) => typeToConvert == RuleQueryMatchCriteriaType; - - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => - RuleQueryMatchCriteriaJsonConverter.Instance; -} - -internal sealed class RuleQueryMatchCriteriaJsonConverter : JsonConverter -{ - public static readonly RuleQueryMatchCriteriaJsonConverter Instance = new(); - - public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - throw new NotSupportedException(); - - public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) - { - if (value is null) - { - writer.WriteNullValue(); - return; - } - - var queryString = RuleQueryMatchCriteriaAccessors.GetQueryString(value); - writer.WriteStartObject(); - writer.WriteString("query_string", queryString); - writer.WriteEndObject(); - } -} - -internal static class RuleQueryMatchCriteriaAccessors -{ - [DynamicDependency( - DynamicallyAccessedMemberTypes.PublicProperties, - "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria", - "Elastic.Internal.Search.Elasticsearch")] - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] - private static readonly Type RuleQueryMatchCriteriaType = Type.GetType( - "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, Elastic.Internal.Search.Elasticsearch", - throwOnError: true)!; - - private static readonly PropertyInfo QueryStringProperty = RuleQueryMatchCriteriaType.GetProperty( - "QueryString", - BindingFlags.Public | BindingFlags.Instance)!; - - [UnconditionalSuppressMessage( - "Trimming", - "IL2075", - Justification = "RuleQueryMatchCriteria.QueryString is preserved via DynamicDependency on RuleQueryMatchCriteriaType.")] - public static string GetQueryString(object target) => (string)QueryStringProperty.GetValue(target)!; -} diff --git a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs deleted file mode 100644 index a2e4291d83..0000000000 --- a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs +++ /dev/null @@ -1,46 +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.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -namespace Elastic.Documentation.Search.Common; - -/// -/// Supplies for internal Elasticsearch search query types not declared on public contexts. -/// -internal sealed class RuleQueryMatchCriteriaTypeInfoResolver(IJsonTypeInfoResolver inner) : IJsonTypeInfoResolver -{ - [DynamicDependency( - DynamicallyAccessedMemberTypes.All, - "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria", - "Elastic.Internal.Search.Elasticsearch")] - private static readonly Type RuleQueryMatchCriteriaType = Type.GetType( - "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, Elastic.Internal.Search.Elasticsearch", - throwOnError: true)!; - - private JsonTypeInfo? _ruleQueryMatchCriteriaTypeInfo; - - public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) - { - if (type == RuleQueryMatchCriteriaType) - { - return _ruleQueryMatchCriteriaTypeInfo ??= CreateRuleQueryMatchCriteriaTypeInfo(options); - } - - return inner.GetTypeInfo(type, options); - } - - [UnconditionalSuppressMessage( - "Trimming", - "IL2026", - Justification = "RuleQueryMatchCriteria is internal to Elastic.Internal.Search.Elasticsearch; serialization uses an UnsafeAccessor-based converter.")] - [UnconditionalSuppressMessage( - "AOT", - "IL3050", - Justification = "RuleQueryMatchCriteria is internal to Elastic.Internal.Search.Elasticsearch; serialization uses an UnsafeAccessor-based converter.")] - private static JsonTypeInfo CreateRuleQueryMatchCriteriaTypeInfo(JsonSerializerOptions options) => - JsonTypeInfo.CreateJsonTypeInfo(RuleQueryMatchCriteriaType, options); -} diff --git a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs index 1a08616052..89c10be04a 100644 --- a/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs +++ b/tests-integration/Mcp.Remote.IntegrationTests/McpToolsIntegrationTestsBase.cs @@ -78,9 +78,7 @@ protected async Task LogIndexCount(ElasticsearchClientAccessor clientAccessor, C return (coherenceTools, clientAccessor); } - private static FullSearchService BuildFullSearchAdapter( - ElasticsearchClientAccessor clientAccessor, - Elastic.Documentation.Configuration.Products.ProductsConfiguration productsConfig) + private static FullSearchService BuildFullSearchAdapter(ElasticsearchClientAccessor clientAccessor, ProductsConfiguration productsConfig) { var sharedConfig = new Elastic.Internal.Search.Configuration.SearchConfiguration { @@ -101,7 +99,7 @@ private static FullSearchService BuildFullSearchAdapter( /// private static ElasticsearchClientAccessor CreateElasticsearchClientAccessor() { - var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler", environment: "dev"); + var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler"); var searchConfig = new SearchConfiguration { diff --git a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs index 33283c84be..a08884c372 100644 --- a/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs +++ b/tests-integration/Search.IntegrationTests/SearchRelevanceTests.cs @@ -243,7 +243,7 @@ public async Task ExplainTopResultAndExpectedAsyncReturnsDetailedScoring() /// private static (NavigationSearchService Gateway, ElasticsearchClientAccessor ClientAccessor) CreateFindPageGateway() { - var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler", environment: "dev"); + var endpoints = ElasticsearchEndpointFactory.Create(buildType: "assembler"); var configProvider = new ConfigurationFileProvider(NullLoggerFactory.Instance, new FileSystem(), configurationSource: ConfigurationSource.Embedded); var searchConfig = configProvider.CreateSearchConfiguration();