Skip to content

Add trace logging for component schema generation #1709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace JsonApiDotNetCore.OpenApi.Swashbuckle;

internal interface ISchemaGenerationTraceScope : IDisposable
{
void TraceSucceeded(string schemaId);
}
153 changes: 153 additions & 0 deletions src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerationTracer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCore.OpenApi.Swashbuckle;

/// <summary>
/// Enables to log recursive component schema generation at trace level.
/// </summary>
internal sealed partial class SchemaGenerationTracer
{
private readonly ILoggerFactory _loggerFactory;

public SchemaGenerationTracer(ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(loggerFactory);

_loggerFactory = loggerFactory;
}

public ISchemaGenerationTraceScope TraceStart(object generator)
{
ArgumentNullException.ThrowIfNull(generator);

return InnerTraceStart(generator, () => "(none)");
}

public ISchemaGenerationTraceScope TraceStart(object generator, Type schemaType)
{
ArgumentNullException.ThrowIfNull(generator);
ArgumentNullException.ThrowIfNull(schemaType);

return InnerTraceStart(generator, () => GetSchemaTypeName(schemaType));
}

public ISchemaGenerationTraceScope TraceStart(object generator, AtomicOperationCode operationCode)
{
ArgumentNullException.ThrowIfNull(generator);

return InnerTraceStart(generator, () => $"{nameof(AtomicOperationCode)}.{operationCode}");
}

public ISchemaGenerationTraceScope TraceStart(object generator, RelationshipAttribute relationship)
{
ArgumentNullException.ThrowIfNull(generator);
ArgumentNullException.ThrowIfNull(relationship);

return InnerTraceStart(generator,
() => $"{GetSchemaTypeName(relationship.GetType())}({GetSchemaTypeName(relationship.LeftType.ClrType)}.{relationship.Property.Name})");
}

public ISchemaGenerationTraceScope TraceStart(object generator, Type schemaOpenType, RelationshipAttribute relationship)
{
ArgumentNullException.ThrowIfNull(generator);
ArgumentNullException.ThrowIfNull(schemaOpenType);
ArgumentNullException.ThrowIfNull(relationship);

return InnerTraceStart(generator,
() =>
$"{GetSchemaTypeName(schemaOpenType)} with {GetSchemaTypeName(relationship.GetType())}({GetSchemaTypeName(relationship.LeftType.ClrType)}.{relationship.Property.Name})");
}

private ISchemaGenerationTraceScope InnerTraceStart(object generator, Func<string> getSchemaTypeName)
{
ILogger logger = _loggerFactory.CreateLogger(generator.GetType());

if (logger.IsEnabled(LogLevel.Trace))
{
string schemaTypeName = getSchemaTypeName();
return new SchemaGenerationTraceScope(logger, schemaTypeName);
}

return DisabledSchemaGenerationTraceScope.Instance;
}

private static string GetSchemaTypeName(Type type)
{
if (type.IsConstructedGenericType)
{
string typeArguments = string.Join(',', type.GetGenericArguments().Select(GetSchemaTypeName));
int arityIndex = type.Name.IndexOf('`');
return $"{type.Name[..arityIndex]}<{typeArguments}>";
}

return type.Name;
}

private sealed partial class SchemaGenerationTraceScope : ISchemaGenerationTraceScope
{
private static readonly AsyncLocal<int> RecursionDepthAsyncLocal = new();

private readonly ILogger _logger;
private readonly string _schemaTypeName;
private string? _schemaId;

public SchemaGenerationTraceScope(ILogger logger, string schemaTypeName)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(schemaTypeName);

_logger = logger;
_schemaTypeName = schemaTypeName;

RecursionDepthAsyncLocal.Value++;
LogStarted(RecursionDepthAsyncLocal.Value, _schemaTypeName);
}

public void TraceSucceeded(string schemaId)
{
_schemaId = schemaId;
}

public void Dispose()
{
if (_schemaId != null)
{
LogSucceeded(RecursionDepthAsyncLocal.Value, _schemaTypeName, _schemaId);
}
else
{
LogFailed(RecursionDepthAsyncLocal.Value, _schemaTypeName);
}

RecursionDepthAsyncLocal.Value--;
}

[LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "({Depth:D2}) Started for {SchemaTypeName}.")]
private partial void LogStarted(int depth, string schemaTypeName);

[LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "({Depth:D2}) Generated '{SchemaId}' from {SchemaTypeName}.")]
private partial void LogSucceeded(int depth, string schemaTypeName, string schemaId);

[LoggerMessage(Level = LogLevel.Trace, SkipEnabledCheck = true, Message = "({Depth:D2}) Failed for {SchemaTypeName}.")]
private partial void LogFailed(int depth, string schemaTypeName);
}

private sealed class DisabledSchemaGenerationTraceScope : ISchemaGenerationTraceScope
{
public static DisabledSchemaGenerationTraceScope Instance { get; } = new();

private DisabledSchemaGenerationTraceScope()
{
}

public void TraceSucceeded(string schemaId)
{
}

public void Dispose()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators.Components;

internal sealed class AtomicOperationCodeSchemaGenerator
{
private readonly SchemaGenerationTracer _schemaGenerationTracer;
private readonly JsonApiSchemaIdSelector _schemaIdSelector;

public AtomicOperationCodeSchemaGenerator(JsonApiSchemaIdSelector schemaIdSelector)
public AtomicOperationCodeSchemaGenerator(SchemaGenerationTracer schemaGenerationTracer, JsonApiSchemaIdSelector schemaIdSelector)
{
ArgumentNullException.ThrowIfNull(schemaGenerationTracer);
ArgumentNullException.ThrowIfNull(schemaIdSelector);

_schemaGenerationTracer = schemaGenerationTracer;
_schemaIdSelector = schemaIdSelector;
}

Expand All @@ -34,6 +37,8 @@ public OpenApiSchema GenerateSchema(AtomicOperationCode operationCode, SchemaRep
};
}

using ISchemaGenerationTraceScope traceScope = _schemaGenerationTracer.TraceStart(this, operationCode);

string enumValue = operationCode.ToString().ToLowerInvariant();

var fullSchema = new OpenApiSchema
Expand All @@ -42,6 +47,9 @@ public OpenApiSchema GenerateSchema(AtomicOperationCode operationCode, SchemaRep
Enum = [new OpenApiString(enumValue)]
};

return schemaRepository.AddDefinition(schemaId, fullSchema);
OpenApiSchema referenceSchema = schemaRepository.AddDefinition(schemaId, fullSchema);

traceScope.TraceSucceeded(schemaId);
return referenceSchema;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators.Components;
/// </summary>
internal sealed class DataContainerSchemaGenerator
{
private readonly SchemaGenerationTracer _schemaGenerationTracer;
private readonly DataSchemaGenerator _dataSchemaGenerator;
private readonly IResourceGraph _resourceGraph;

public DataContainerSchemaGenerator(DataSchemaGenerator dataSchemaGenerator, IResourceGraph resourceGraph)
public DataContainerSchemaGenerator(SchemaGenerationTracer schemaGenerationTracer, DataSchemaGenerator dataSchemaGenerator, IResourceGraph resourceGraph)
{
ArgumentNullException.ThrowIfNull(schemaGenerationTracer);
ArgumentNullException.ThrowIfNull(dataSchemaGenerator);
ArgumentNullException.ThrowIfNull(resourceGraph);

_schemaGenerationTracer = schemaGenerationTracer;
_dataSchemaGenerator = dataSchemaGenerator;
_resourceGraph = resourceGraph;
}
Expand All @@ -45,6 +48,13 @@ public OpenApiSchema GenerateSchema(Type dataContainerSchemaType, ResourceType r

Type dataConstructedType = GetElementTypeOfDataProperty(dataContainerSchemaType, resourceType);

if (schemaRepository.TryLookupByType(dataConstructedType, out _))
{
return referenceSchemaForData;
}

using ISchemaGenerationTraceScope traceScope = _schemaGenerationTracer.TraceStart(this, dataConstructedType);

if (canIncludeRelated)
{
var resourceSchemaType = ResourceSchemaType.Create(dataConstructedType, _resourceGraph);
Expand All @@ -57,7 +67,9 @@ public OpenApiSchema GenerateSchema(Type dataContainerSchemaType, ResourceType r
}
}

return _dataSchemaGenerator.GenerateSchema(dataConstructedType, forRequestSchema, schemaRepository);
referenceSchemaForData = _dataSchemaGenerator.GenerateSchema(dataConstructedType, forRequestSchema, schemaRepository);
traceScope.TraceSucceeded(referenceSchemaForData.Reference.Id);
return referenceSchemaForData;
}

private static Type GetElementTypeOfDataProperty(Type dataContainerConstructedType, ResourceType resourceType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal sealed class DataSchemaGenerator
JsonApiPropertyName.Meta
];

private readonly SchemaGenerationTracer _schemaGenerationTracer;
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly GenerationCacheSchemaGenerator _generationCacheSchemaGenerator;
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
Expand All @@ -42,12 +43,14 @@ internal sealed class DataSchemaGenerator
private readonly RelationshipTypeFactory _relationshipTypeFactory;
private readonly ResourceDocumentationReader _resourceDocumentationReader;

public DataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, GenerationCacheSchemaGenerator generationCacheSchemaGenerator,
ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, ResourceIdSchemaGenerator resourceIdSchemaGenerator,
LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator, MetaSchemaGenerator metaSchemaGenerator, JsonApiSchemaIdSelector schemaIdSelector,
IJsonApiOptions options, IResourceGraph resourceGraph, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider,
RelationshipTypeFactory relationshipTypeFactory, ResourceDocumentationReader resourceDocumentationReader)
public DataSchemaGenerator(SchemaGenerationTracer schemaGenerationTracer, SchemaGenerator defaultSchemaGenerator,
GenerationCacheSchemaGenerator generationCacheSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator,
ResourceIdSchemaGenerator resourceIdSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
MetaSchemaGenerator metaSchemaGenerator, JsonApiSchemaIdSelector schemaIdSelector, IJsonApiOptions options, IResourceGraph resourceGraph,
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider, RelationshipTypeFactory relationshipTypeFactory,
ResourceDocumentationReader resourceDocumentationReader)
{
ArgumentNullException.ThrowIfNull(schemaGenerationTracer);
ArgumentNullException.ThrowIfNull(defaultSchemaGenerator);
ArgumentNullException.ThrowIfNull(generationCacheSchemaGenerator);
ArgumentNullException.ThrowIfNull(resourceTypeSchemaGenerator);
Expand All @@ -61,6 +64,7 @@ public DataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, GenerationCac
ArgumentNullException.ThrowIfNull(relationshipTypeFactory);
ArgumentNullException.ThrowIfNull(resourceDocumentationReader);

_schemaGenerationTracer = schemaGenerationTracer;
_defaultSchemaGenerator = defaultSchemaGenerator;
_generationCacheSchemaGenerator = generationCacheSchemaGenerator;
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
Expand Down Expand Up @@ -108,6 +112,8 @@ public OpenApiSchema GenerateSchema(Type dataSchemaType, bool forRequestSchema,
return schemaRepository.LookupByType(dataSchemaType);
}

using ISchemaGenerationTraceScope traceScope = _schemaGenerationTracer.TraceStart(this, dataSchemaType);

referenceSchemaForData = _defaultSchemaGenerator.GenerateSchema(dataSchemaType, schemaRepository);
OpenApiSchema fullSchemaForData = schemaRepository.Schemas[referenceSchemaForData.Reference.Id];
fullSchemaForData.AdditionalPropertiesAllowed = false;
Expand Down Expand Up @@ -139,6 +145,7 @@ public OpenApiSchema GenerateSchema(Type dataSchemaType, bool forRequestSchema,
fullSchemaForData.Extensions[SetSchemaTypeToObjectDocumentFilter.RequiresRootObjectTypeKey] = new OpenApiBoolean(true);
}

traceScope.TraceSucceeded(referenceSchemaForData.Reference.Id);
return referenceSchemaForData;
}

Expand Down Expand Up @@ -205,6 +212,8 @@ public OpenApiSchema GenerateSchemaForCommonData(Type commonDataSchemaType, Sche
return referenceSchema;
}

using ISchemaGenerationTraceScope traceScope = _schemaGenerationTracer.TraceStart(this, commonDataSchemaType);

OpenApiSchema referenceSchemaForResourceType = _resourceTypeSchemaGenerator.GenerateSchema(schemaRepository);
OpenApiSchema referenceSchemaForMeta = _metaSchemaGenerator.GenerateSchema(schemaRepository);

Expand Down Expand Up @@ -234,6 +243,7 @@ public OpenApiSchema GenerateSchemaForCommonData(Type commonDataSchemaType, Sche
referenceSchema = schemaRepository.AddDefinition(schemaId, fullSchema);
schemaRepository.RegisterType(commonDataSchemaType, schemaId);

traceScope.TraceSucceeded(schemaId);
return referenceSchema;
}

Expand Down Expand Up @@ -344,7 +354,7 @@ private void SetResourceFields(OpenApiSchema fullSchemaForData, ResourceSchemaTy

if (schemaHasFields)
{
var fieldSchemaBuilder = new ResourceFieldSchemaBuilder(_defaultSchemaGenerator, this, _linksVisibilitySchemaGenerator,
var fieldSchemaBuilder = new ResourceFieldSchemaBuilder(_schemaGenerationTracer, _defaultSchemaGenerator, this, _linksVisibilitySchemaGenerator,
_resourceFieldValidationMetadataProvider, _relationshipTypeFactory, resourceSchemaType);

SetFieldSchemaMembers(fullSchemaForData, resourceSchemaType, forRequestSchema, true, fieldSchemaBuilder, schemaRepository);
Expand Down Expand Up @@ -434,6 +444,8 @@ private OpenApiSchema GenerateSchemaForCommonFields(Type commonFieldsSchemaType,
return referenceSchema;
}

using ISchemaGenerationTraceScope traceScope = _schemaGenerationTracer.TraceStart(this, commonFieldsSchemaType);

OpenApiSchema referenceSchemaForResourceType = _resourceTypeSchemaGenerator.GenerateSchema(schemaRepository);

var fullSchema = new OpenApiSchema
Expand Down Expand Up @@ -461,6 +473,7 @@ private OpenApiSchema GenerateSchemaForCommonFields(Type commonFieldsSchemaType,
referenceSchema = schemaRepository.AddDefinition(schemaId, fullSchema);
schemaRepository.RegisterType(commonFieldsSchemaType, schemaId);

traceScope.TraceSucceeded(schemaId);
return referenceSchema;
}

Expand Down Expand Up @@ -557,6 +570,8 @@ private void GenerateDataSchemasForDirectlyDerivedTypes(ResourceSchemaType resou
ResourceSchemaType resourceSchemaTypeForDerived = resourceSchemaType.ChangeResourceType(derivedType);
Type derivedSchemaType = resourceSchemaTypeForDerived.SchemaConstructedType;

using ISchemaGenerationTraceScope traceScope = _schemaGenerationTracer.TraceStart(this, resourceSchemaTypeForDerived.SchemaConstructedType);

OpenApiSchema referenceSchemaForDerived = _defaultSchemaGenerator.GenerateSchema(derivedSchemaType, schemaRepository);
OpenApiSchema fullSchemaForDerived = schemaRepository.Schemas[referenceSchemaForDerived.Reference.Id];
fullSchemaForDerived.AdditionalPropertiesAllowed = false;
Expand Down Expand Up @@ -594,6 +609,8 @@ private void GenerateDataSchemasForDirectlyDerivedTypes(ResourceSchemaType resou
}

GenerateDataSchemasForDirectlyDerivedTypes(resourceSchemaTypeForDerived, forRequestSchema, schemaRepository);

traceScope.TraceSucceeded(referenceSchemaForDerived.Reference.Id);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators.Components;

internal sealed class MetaSchemaGenerator
{
private static readonly Type SchemaType = typeof(Meta);
private readonly SchemaGenerationTracer _schemaGenerationTracer;
private readonly JsonApiSchemaIdSelector _schemaIdSelector;

public MetaSchemaGenerator(JsonApiSchemaIdSelector schemaIdSelector)
public MetaSchemaGenerator(SchemaGenerationTracer schemaGenerationTracer, JsonApiSchemaIdSelector schemaIdSelector)
{
ArgumentNullException.ThrowIfNull(schemaGenerationTracer);
ArgumentNullException.ThrowIfNull(schemaIdSelector);

_schemaGenerationTracer = schemaGenerationTracer;
_schemaIdSelector = schemaIdSelector;
}

public OpenApiSchema GenerateSchema(SchemaRepository schemaRepository)
{
ArgumentNullException.ThrowIfNull(schemaRepository);

if (schemaRepository.TryLookupByType(typeof(Meta), out OpenApiSchema? referenceSchema))
if (schemaRepository.TryLookupByType(SchemaType, out OpenApiSchema? referenceSchema))
{
return referenceSchema;
}

using ISchemaGenerationTraceScope traceScope = _schemaGenerationTracer.TraceStart(this, SchemaType);

var fullSchema = new OpenApiSchema
{
Type = "object",
Expand All @@ -36,8 +42,9 @@ public OpenApiSchema GenerateSchema(SchemaRepository schemaRepository)
string schemaId = _schemaIdSelector.GetMetaSchemaId();

referenceSchema = schemaRepository.AddDefinition(schemaId, fullSchema);
schemaRepository.RegisterType(typeof(Meta), schemaId);
schemaRepository.RegisterType(SchemaType, schemaId);

traceScope.TraceSucceeded(schemaId);
return referenceSchema;
}
}
Loading
Loading