Files
git.stella-ops.org/src/Signals/StellaOps.Signals/Services/CallgraphIngestionService.cs
master 69c59defdc
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat: Implement Runtime Facts ingestion service and NDJSON reader
- 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.
2025-11-10 07:56:15 +02:00

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)
{
}
}