nuget reorganization

This commit is contained in:
master
2025-11-18 23:45:25 +02:00
parent 77cee6a209
commit d3ecd7f8e6
7712 changed files with 13963 additions and 10007504 deletions

View File

@@ -85,3 +85,12 @@ public sealed record ComponentLookupResult(
IReadOnlyList<ComponentNeighbor> Neighbors,
string? NextCursor,
string CacheHint);
public sealed record ComponentLookupRecord(
string Artifact,
string Purl,
string NeighborPurl,
string Relationship,
string? License,
string Scope,
bool RuntimeFlag);

View File

@@ -0,0 +1,11 @@
# SBOM Service Observability
Artifacts added for SBOM-AIAI-31-002 (Advisory AI endpoints):
- `sbomservice-grafana-dashboard.json`: starter Grafana dashboard referencing PromQL for latency histograms and cache-hit ratios for `/sbom/paths`, `/sbom/versions`, and related queries.
Notes:
- Metrics names match Program.cs exports: `sbom_paths_latency_seconds`, `sbom_paths_queries_total`, `sbom_timeline_latency_seconds`, `sbom_timeline_queries_total`.
- Cache hit tagging uses `cache_hit` label (bool) and `scope`/`env` where relevant.
- Dashboard is schemaVersion 39; adjust datasource UID at import.
- Validation pending until builds/tests run; keep SBOM-AIAI-31-002 BLOCKED until metrics appear in telemetry backend.

View File

@@ -0,0 +1,46 @@
{
"uid": "sbomservice-runtime",
"title": "SBOM Service Runtime",
"schemaVersion": 39,
"version": 1,
"panels": [
{
"type": "timeseries",
"title": "Paths latency (p50/p90)",
"targets": [
{ "expr": "histogram_quantile(0.5, sum by (le) (rate(sbom_paths_latency_seconds_bucket[5m])))" },
{ "expr": "histogram_quantile(0.9, sum by (le) (rate(sbom_paths_latency_seconds_bucket[5m])))" }
]
},
{
"type": "timeseries",
"title": "Timeline latency (p50/p90)",
"targets": [
{ "expr": "histogram_quantile(0.5, sum by (le) (rate(sbom_timeline_latency_seconds_bucket[5m])))" },
{ "expr": "histogram_quantile(0.9, sum by (le) (rate(sbom_timeline_latency_seconds_bucket[5m])))" }
]
},
{
"type": "stat",
"title": "Paths cache hit ratio",
"targets": [
{ "expr": "sum(rate(sbom_paths_queries_total{cache_hit='true'}[5m])) / sum(rate(sbom_paths_queries_total[5m]))" }
]
},
{
"type": "stat",
"title": "Timeline cache hit ratio",
"targets": [
{ "expr": "sum(rate(sbom_timeline_queries_total{cache_hit='true'}[5m])) / sum(rate(sbom_timeline_queries_total[5m]))" }
]
},
{
"type": "stat",
"title": "Console cache hint",
"targets": [
{ "expr": "sum(rate(sbom_paths_queries_total{scope!=''}[5m]))" }
]
}
],
"time": { "from": "now-6h", "to": "now" }
}

View File

@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Services;
using StellaOps.SbomService.Observability;
using StellaOps.SbomService.Repositories;
var builder = WebApplication.CreateBuilder(args);
@@ -16,6 +17,15 @@ builder.Services.AddOptions();
builder.Services.AddLogging();
// Register SBOM query services (InMemory seed; replace with Mongo-backed repository later).
builder.Services.AddSingleton<IComponentLookupRepository>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var mongoConn = config.GetConnectionString("SbomServiceMongo") ?? "mongodb://localhost:27017";
var mongoClient = new MongoDB.Driver.MongoClient(mongoConn);
var databaseName = config.GetSection("SbomService")?["Database"] ?? "sbomservice";
var database = mongoClient.GetDatabase(databaseName);
return new MongoComponentLookupRepository(database);
});
builder.Services.AddSingleton<ISbomQueryService, InMemorySbomQueryService>();
var app = builder.Build();

View File

@@ -0,0 +1,8 @@
using StellaOps.SbomService.Models;
namespace StellaOps.SbomService.Repositories;
public interface IComponentLookupRepository
{
Task<IReadOnlyList<ComponentLookupRecord>> QueryAsync(ComponentLookupQuery query, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,56 @@
using StellaOps.SbomService.Models;
namespace StellaOps.SbomService.Repositories;
internal sealed class InMemoryComponentLookupRepository : IComponentLookupRepository
{
private static readonly IReadOnlyList<ComponentLookupRecord> Components = Seed();
public Task<IReadOnlyList<ComponentLookupRecord>> QueryAsync(ComponentLookupQuery query, CancellationToken cancellationToken)
{
var filtered = Components
.Where(c => c.Purl.Equals(query.Purl, StringComparison.OrdinalIgnoreCase))
.Where(c => query.Artifact is null || c.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase))
.OrderBy(c => c.Artifact)
.ThenBy(c => c.Purl)
.ToList();
var page = filtered
.Skip(query.Offset)
.Take(query.Limit)
.ToList();
return Task.FromResult<IReadOnlyList<ComponentLookupRecord>>(page);
}
private static IReadOnlyList<ComponentLookupRecord> Seed()
{
return new List<ComponentLookupRecord>
{
new(
Artifact: "ghcr.io/stellaops/sample-api",
Purl: "pkg:npm/lodash@4.17.21",
NeighborPurl: "pkg:npm/express@4.18.2",
Relationship: "DEPENDS_ON",
License: "MIT",
Scope: "runtime",
RuntimeFlag: true),
new(
Artifact: "ghcr.io/stellaops/sample-api",
Purl: "pkg:npm/lodash@4.17.21",
NeighborPurl: "pkg:npm/rollup@3.0.0",
Relationship: "DEPENDS_ON",
License: "MIT",
Scope: "build",
RuntimeFlag: false),
new(
Artifact: "ghcr.io/stellaops/sample-worker",
Purl: "pkg:nuget/Newtonsoft.Json@13.0.2",
NeighborPurl: "pkg:nuget/StellaOps.Core@1.0.0",
Relationship: "DEPENDS_ON",
License: "Apache-2.0",
Scope: "runtime",
RuntimeFlag: true)
};
}
}

View File

@@ -0,0 +1,33 @@
using MongoDB.Driver;
using StellaOps.SbomService.Models;
namespace StellaOps.SbomService.Repositories;
internal sealed class MongoComponentLookupRepository : IComponentLookupRepository
{
private readonly IMongoCollection<ComponentLookupRecord> _collection;
public MongoComponentLookupRepository(IMongoDatabase database)
{
_collection = database.GetCollection<ComponentLookupRecord>("sbom_components");
}
public async Task<IReadOnlyList<ComponentLookupRecord>> QueryAsync(ComponentLookupQuery query, CancellationToken cancellationToken)
{
var filter = Builders<ComponentLookupRecord>.Filter.Eq(c => c.Purl, query.Purl);
if (!string.IsNullOrWhiteSpace(query.Artifact))
{
filter &= Builders<ComponentLookupRecord>.Filter.Eq(c => c.Artifact, query.Artifact);
}
var results = await _collection
.Find(filter)
.Skip(query.Offset)
.Limit(query.Limit)
.Sort(Builders<ComponentLookupRecord>.Sort.Ascending(c => c.Artifact).Ascending(c => c.NeighborPurl))
.ToListAsync(cancellationToken);
return results;
}
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Concurrent;
using System.Globalization;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Repositories;
namespace StellaOps.SbomService.Services;
@@ -8,16 +10,16 @@ internal sealed class InMemorySbomQueryService : ISbomQueryService
private readonly IReadOnlyList<PathRecord> _paths;
private readonly IReadOnlyList<TimelineRecord> _timelines;
private readonly IReadOnlyList<CatalogRecord> _catalog;
private readonly IReadOnlyList<ComponentLookupRecord> _components;
private readonly IComponentLookupRepository _componentLookupRepository;
private readonly ConcurrentDictionary<string, object> _cache = new();
public InMemorySbomQueryService()
public InMemorySbomQueryService(IComponentLookupRepository componentLookupRepository)
{
_componentLookupRepository = componentLookupRepository;
// Deterministic seed data for early contract testing; replace with Mongo-backed implementation later.
_paths = SeedPaths();
_timelines = SeedTimelines();
_catalog = SeedCatalog();
_components = SeedComponents();
}
public Task<QueryResult<SbomPathResult>> GetPathsAsync(SbomPathQuery query, CancellationToken cancellationToken)
@@ -131,34 +133,27 @@ internal sealed class InMemorySbomQueryService : ISbomQueryService
return Task.FromResult(new QueryResult<SbomCatalogResult>(result, false));
}
public Task<QueryResult<ComponentLookupResult>> GetComponentLookupAsync(ComponentLookupQuery query, CancellationToken cancellationToken)
public async Task<QueryResult<ComponentLookupResult>> GetComponentLookupAsync(ComponentLookupQuery query, CancellationToken cancellationToken)
{
var cacheKey = $"component|{query.Purl}|{query.Artifact}|{query.Offset}|{query.Limit}";
if (_cache.TryGetValue(cacheKey, out var cached) && cached is ComponentLookupResult cachedResult)
{
return Task.FromResult(new QueryResult<ComponentLookupResult>(cachedResult, true));
return new QueryResult<ComponentLookupResult>(cachedResult, true);
}
var filtered = _components
.Where(c => c.Purl.Equals(query.Purl, StringComparison.OrdinalIgnoreCase))
.Where(c => query.Artifact is null || c.Artifact.Equals(query.Artifact, StringComparison.OrdinalIgnoreCase))
.OrderBy(c => c.Artifact)
.ThenBy(c => c.Purl)
.ToList();
var page = await _componentLookupRepository.QueryAsync(query, cancellationToken);
var page = filtered
.Skip(query.Offset)
.Take(query.Limit)
.Select(c => new ComponentNeighbor(c.NeighborPurl, c.Relationship, c.License, c.Scope, c.RuntimeFlag))
.ToList();
string? nextCursor = query.Offset + query.Limit < filtered.Count
string? nextCursor = query.Offset + query.Limit < page.Count
? (query.Offset + query.Limit).ToString(CultureInfo.InvariantCulture)
: null;
var result = new ComponentLookupResult(query.Purl, query.Artifact, page, nextCursor, CacheHint: "seeded");
var neighbors = page
.Select(c => new ComponentNeighbor(c.NeighborPurl, c.Relationship, c.License, c.Scope, c.RuntimeFlag))
.ToList();
var result = new ComponentLookupResult(query.Purl, query.Artifact, neighbors, nextCursor, CacheHint: "seeded");
_cache[cacheKey] = result;
return Task.FromResult(new QueryResult<ComponentLookupResult>(result, false));
return new QueryResult<ComponentLookupResult>(result, false);
}
private static IReadOnlyList<PathRecord> SeedPaths()

View File

@@ -13,4 +13,8 @@
<ProjectReference Include="../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.DependencyInjection/StellaOps.DependencyInjection.csproj" />
</ItemGroup>
</Project>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="2.24.0" />
</ItemGroup>
</Project>