Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
- Added RuntimeFactsNdjsonReader for reading NDJSON formatted runtime facts. - Introduced IRuntimeFactsIngestionService interface and its implementation. - Enhanced Program.cs to register new services and endpoints for runtime facts. - Updated CallgraphIngestionService to include CAS URI in stored artifacts. - Created RuntimeFactsValidationException for validation errors during ingestion. - Added tests for RuntimeFactsIngestionService and RuntimeFactsNdjsonReader. - Implemented SignalsSealedModeMonitor for compliance checks in sealed mode. - Updated project dependencies for testing utilities.
164 lines
6.4 KiB
C#
164 lines
6.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Signals.Models;
|
|
using StellaOps.Signals.Options;
|
|
using StellaOps.Signals.Parsing;
|
|
using StellaOps.Signals.Persistence;
|
|
using StellaOps.Signals.Storage;
|
|
using StellaOps.Signals.Storage.Models;
|
|
|
|
namespace StellaOps.Signals.Services;
|
|
|
|
internal sealed class CallgraphIngestionService : ICallgraphIngestionService
|
|
{
|
|
private static readonly HashSet<string> AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"application/json",
|
|
"application/vnd.stellaops.callgraph+json"
|
|
};
|
|
|
|
private readonly ICallgraphParserResolver parserResolver;
|
|
private readonly ICallgraphArtifactStore artifactStore;
|
|
private readonly ICallgraphRepository repository;
|
|
private readonly ILogger<CallgraphIngestionService> logger;
|
|
private readonly SignalsOptions options;
|
|
private readonly TimeProvider timeProvider;
|
|
|
|
public CallgraphIngestionService(
|
|
ICallgraphParserResolver parserResolver,
|
|
ICallgraphArtifactStore artifactStore,
|
|
ICallgraphRepository repository,
|
|
IOptions<SignalsOptions> options,
|
|
TimeProvider timeProvider,
|
|
ILogger<CallgraphIngestionService> logger)
|
|
{
|
|
this.parserResolver = parserResolver ?? throw new ArgumentNullException(nameof(parserResolver));
|
|
this.artifactStore = artifactStore ?? throw new ArgumentNullException(nameof(artifactStore));
|
|
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
|
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
|
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
|
}
|
|
|
|
public async Task<CallgraphIngestResponse> IngestAsync(CallgraphIngestRequest request, CancellationToken cancellationToken)
|
|
{
|
|
ValidateRequest(request);
|
|
|
|
var parser = parserResolver.Resolve(request.Language);
|
|
|
|
var artifactBytes = Convert.FromBase64String(request.ArtifactContentBase64);
|
|
await using var parseStream = new MemoryStream(artifactBytes, writable: false);
|
|
var parseResult = await parser.ParseAsync(parseStream, cancellationToken).ConfigureAwait(false);
|
|
|
|
parseStream.Position = 0;
|
|
var hash = ComputeSha256(artifactBytes);
|
|
|
|
var artifactMetadata = await artifactStore.SaveAsync(
|
|
new CallgraphArtifactSaveRequest(
|
|
request.Language,
|
|
request.Component,
|
|
request.Version,
|
|
request.ArtifactFileName,
|
|
request.ArtifactContentType,
|
|
hash),
|
|
parseStream,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
var document = new CallgraphDocument
|
|
{
|
|
Language = parser.Language,
|
|
Component = request.Component,
|
|
Version = request.Version,
|
|
Nodes = new List<CallgraphNode>(parseResult.Nodes),
|
|
Edges = new List<CallgraphEdge>(parseResult.Edges),
|
|
Metadata = request.Metadata is null
|
|
? null
|
|
: new Dictionary<string, string?>(request.Metadata, StringComparer.OrdinalIgnoreCase),
|
|
Artifact = new CallgraphArtifactMetadata
|
|
{
|
|
Path = artifactMetadata.Path,
|
|
Hash = artifactMetadata.Hash,
|
|
CasUri = artifactMetadata.CasUri,
|
|
ContentType = artifactMetadata.ContentType,
|
|
Length = artifactMetadata.Length
|
|
},
|
|
IngestedAt = timeProvider.GetUtcNow()
|
|
};
|
|
|
|
document.Metadata ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
|
document.Metadata["formatVersion"] = parseResult.FormatVersion;
|
|
|
|
document = await repository.UpsertAsync(document, cancellationToken).ConfigureAwait(false);
|
|
|
|
logger.LogInformation(
|
|
"Ingested callgraph {Language}:{Component}:{Version} (id={Id}) with {NodeCount} nodes and {EdgeCount} edges.",
|
|
document.Language,
|
|
document.Component,
|
|
document.Version,
|
|
document.Id,
|
|
document.Nodes.Count,
|
|
document.Edges.Count);
|
|
|
|
return new CallgraphIngestResponse(document.Id, document.Artifact.Path, document.Artifact.Hash, document.Artifact.CasUri);
|
|
}
|
|
|
|
private static void ValidateRequest(CallgraphIngestRequest request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Language))
|
|
{
|
|
throw new CallgraphIngestionValidationException("Language is required.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Component))
|
|
{
|
|
throw new CallgraphIngestionValidationException("Component is required.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Version))
|
|
{
|
|
throw new CallgraphIngestionValidationException("Version is required.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(request.ArtifactContentBase64))
|
|
{
|
|
throw new CallgraphIngestionValidationException("Artifact content is required.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(request.ArtifactFileName))
|
|
{
|
|
throw new CallgraphIngestionValidationException("Artifact file name is required.");
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(request.ArtifactContentType) || !AllowedContentTypes.Contains(request.ArtifactContentType))
|
|
{
|
|
throw new CallgraphIngestionValidationException($"Unsupported artifact content type '{request.ArtifactContentType}'.");
|
|
}
|
|
}
|
|
|
|
private static string ComputeSha256(ReadOnlySpan<byte> buffer)
|
|
{
|
|
Span<byte> hash = stackalloc byte[SHA256.HashSizeInBytes];
|
|
SHA256.HashData(buffer, hash);
|
|
return Convert.ToHexString(hash);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exception thrown when the ingestion request is invalid.
|
|
/// </summary>
|
|
public sealed class CallgraphIngestionValidationException : Exception
|
|
{
|
|
public CallgraphIngestionValidationException(string message) : base(message)
|
|
{
|
|
}
|
|
}
|