nuget reorganization
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using StellaOps.SbomService.Models;
|
||||
|
||||
namespace StellaOps.SbomService.Repositories;
|
||||
|
||||
public interface IComponentLookupRepository
|
||||
{
|
||||
Task<IReadOnlyList<ComponentLookupRecord>> QueryAsync(ComponentLookupQuery query, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user