diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 2c57f06da..e56e28cb3 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Caching.Memory; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { @@ -20,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private readonly IContentQueryService contentQuery; private readonly IGraphQLUrlGenerator urlGenerator; + private readonly ISemanticLog log; private readonly IAssetQueryService assetQuery; private readonly IAppProvider appProvider; @@ -28,18 +30,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL IAppProvider appProvider, IAssetQueryService assetQuery, IContentQueryService contentQuery, - IGraphQLUrlGenerator urlGenerator) + IGraphQLUrlGenerator urlGenerator, + ISemanticLog log) : base(cache) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(contentQuery, nameof(contentQuery)); Guard.NotNull(urlGenerator, nameof(urlGenerator)); + Guard.NotNull(log, nameof(log)); this.appProvider = appProvider; this.assetQuery = assetQuery; this.contentQuery = contentQuery; this.urlGenerator = urlGenerator; + this.log = log; } public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries) @@ -70,14 +75,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return result; } - private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) + private async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) { if (string.IsNullOrWhiteSpace(query.Query)) { return (false, new { data = new object() }); } - var result = await model.ExecuteAsync(ctx, query); + var result = await model.ExecuteAsync(ctx, query, log); if (result.Errors?.Any() == true) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index a07e8aca3..1373ee437 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; using GraphQLSchema = GraphQL.Types.Schema; #pragma warning disable IDE0003 @@ -171,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return contentTypes.GetOrAdd(schema, s => new ContentGraphType()); } - public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) + public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query, ISemanticLog log) { Guard.NotNull(context, nameof(context)); @@ -179,6 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var result = await new DocumentExecuter().ExecuteAsync(options => { + options.FieldMiddleware.Use(LoggingMiddleware.Create(log)); options.OperationName = query.OperationName; options.UserContext = context; options.Schema = graphQLSchema; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs new file mode 100644 index 000000000..9db32cab0 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/LoggingMiddleware.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Instrumentation; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public static class LoggingMiddleware + { + public static Func Create(ISemanticLog log) + { + Guard.NotNull(log, nameof(log)); + + return new Func(next => + { + return async context => + { + try + { + return await next(context); + } + catch (Exception ex) + { + log.LogWarning(ex, w => w + .WriteProperty("action", "reolveField") + .WriteProperty("status", "failed") + .WriteProperty("field", context.FieldName)); + + throw ex; + } + }; + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index d3f6b5ea1..53e155449 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -24,6 +24,7 @@ using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Log; using Xunit; #pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter @@ -96,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas); - sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator()); + sut = new CachingGraphQLService(cache, appProvider, assetQuery, contentQuery, new FakeUrlGenerator(), A.Fake()); } protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null)