mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(wiki): add WikiMetrics gateway for compile/relation/retrieval/vision
This commit is contained in:
parent
8c82f2fde3
commit
19b82cdc2a
@ -0,0 +1,189 @@
|
||||
package vip.mate.wiki.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Centralized wiki metrics gateway.
|
||||
*
|
||||
* <p>All wiki-domain metrics flow through this Bean so we can:
|
||||
* <ul>
|
||||
* <li>Standardize tag names ({@code kb_id}, {@code stage}, {@code provider})</li>
|
||||
* <li>Gracefully no-op when MeterRegistry is absent (test env)</li>
|
||||
* <li>Add cross-cutting concerns (cardinality limits, sampling) in one place</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Mirrors the decorator pattern used in {@code vip.mate.memory.spi.decorator.MetricsMemoryProvider}
|
||||
* but is service-flavored rather than decorator-flavored: callers inject this bean and
|
||||
* push metrics through explicit methods rather than wrapping a delegate.
|
||||
*
|
||||
* <p>Method families:
|
||||
* <ul>
|
||||
* <li>{@code recordCompile*} — wiki compile pipeline timings & cache outcomes</li>
|
||||
* <li>{@code recordRelation*} — page-to-page relation computation & cache</li>
|
||||
* <li>{@code recordRetrieval} — hybrid retrieval timings & result counts</li>
|
||||
* <li>{@code recordVision*} — image OCR / vision-in calls & cache</li>
|
||||
* <li>{@link #startTimer} — generic try-with-resources timer for ad-hoc spans</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WikiMetrics {
|
||||
|
||||
private final MeterRegistry registry;
|
||||
private final ConcurrentHashMap<String, Counter> counterCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Timer> timerCache = new ConcurrentHashMap<>();
|
||||
|
||||
public WikiMetrics(ObjectProvider<MeterRegistry> registryProvider) {
|
||||
this.registry = registryProvider.getIfAvailable();
|
||||
if (registry == null) {
|
||||
log.info("[WikiMetrics] MeterRegistry not available; running in no-op mode");
|
||||
} else {
|
||||
log.info("[WikiMetrics] MeterRegistry available; metrics enabled");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Compile pipeline ====================
|
||||
|
||||
/** Records the duration of one compile-pipeline stage. */
|
||||
public void recordCompileStage(String stage, Long kbId, Duration duration) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getTimer("wiki.compile.stage",
|
||||
Tags.of(Tag.of("stage", stage),
|
||||
Tag.of("kb_id", String.valueOf(kbId))))
|
||||
.record(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records LLM prompt-cache outcome for a compile call.
|
||||
*
|
||||
* @param hit whether the call hit a cached prefix
|
||||
* @param tokensSaved when {@code hit==true}, the count of cached input tokens
|
||||
*/
|
||||
public void recordCompileCache(boolean hit, long tokensSaved) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getCounter("wiki.compile.cache.outcome",
|
||||
Tags.of(Tag.of("outcome", hit ? "hit" : "miss"))).increment();
|
||||
if (hit && tokensSaved > 0) {
|
||||
getCounter("wiki.compile.cache.tokens_saved", Tags.empty())
|
||||
.increment(tokensSaved);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Relation / Graph ====================
|
||||
|
||||
public void recordRelationCompute(Long kbId, int nodeCount, Duration duration) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getTimer("wiki.relation.compute",
|
||||
Tags.of(Tag.of("kb_id", String.valueOf(kbId))))
|
||||
.record(duration);
|
||||
getCounter("wiki.relation.compute.invocations", Tags.empty()).increment();
|
||||
// Distribution summary for nodeCount intentionally omitted to keep cardinality bounded;
|
||||
// surface via a separate gauge if needed.
|
||||
if (nodeCount > 0) {
|
||||
getCounter("wiki.relation.compute.nodes", Tags.empty()).increment(nodeCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void recordRelationCacheHit(boolean hit) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getCounter("wiki.relation.cache",
|
||||
Tags.of(Tag.of("outcome", hit ? "hit" : "miss"))).increment();
|
||||
}
|
||||
|
||||
// ==================== Retrieval ====================
|
||||
|
||||
public void recordRetrieval(String mode, Duration duration, int resultCount) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getTimer("wiki.retrieval.duration",
|
||||
Tags.of(Tag.of("mode", mode))).record(duration);
|
||||
if (resultCount > 0) {
|
||||
getCounter("wiki.retrieval.results",
|
||||
Tags.of(Tag.of("mode", mode))).increment(resultCount);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== OCR / Vision ====================
|
||||
|
||||
public void recordVisionCall(String provider, boolean success, Duration duration) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getTimer("wiki.vision.call",
|
||||
Tags.of(Tag.of("provider", provider),
|
||||
Tag.of("outcome", success ? "success" : "failure")))
|
||||
.record(duration);
|
||||
}
|
||||
|
||||
public void recordVisionCacheHit(boolean hit) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getCounter("wiki.vision.cache",
|
||||
Tags.of(Tag.of("outcome", hit ? "hit" : "miss"))).increment();
|
||||
}
|
||||
|
||||
// ==================== Generic try-with-resources timer ====================
|
||||
|
||||
/**
|
||||
* Convenience for ad-hoc timers using the AutoCloseable pattern:
|
||||
*
|
||||
* <pre>{@code
|
||||
* try (var sample = metrics.startTimer("wiki.compile.stage", "stage", "summary")) {
|
||||
* // work...
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public WikiTimerSample startTimer(String name, String... keyValues) {
|
||||
return new WikiTimerSample(this, name, keyValues, System.nanoTime());
|
||||
}
|
||||
|
||||
/** Used by {@link WikiTimerSample#close()} — package-private. */
|
||||
void recordTimer(String name, Tags tags, long durationNanos) {
|
||||
if (registry == null) {
|
||||
return;
|
||||
}
|
||||
getTimer(name, tags).record(Duration.ofNanos(durationNanos));
|
||||
}
|
||||
|
||||
// ==================== Internal cache to avoid registering same meter twice ====================
|
||||
|
||||
private Counter getCounter(String name, Tags tags) {
|
||||
return counterCache.computeIfAbsent(meterKey(name, tags),
|
||||
k -> registry.counter(name, tags));
|
||||
}
|
||||
|
||||
private Timer getTimer(String name, Tags tags) {
|
||||
return timerCache.computeIfAbsent(meterKey(name, tags),
|
||||
k -> registry.timer(name, tags));
|
||||
}
|
||||
|
||||
private static String meterKey(String name, Tags tags) {
|
||||
StringBuilder sb = new StringBuilder(name);
|
||||
for (Tag t : tags) {
|
||||
sb.append('|').append(t.getKey()).append('=').append(t.getValue());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package vip.mate.wiki.metrics;
|
||||
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
|
||||
/**
|
||||
* AutoCloseable timer sample for try-with-resources usage.
|
||||
*
|
||||
* <p>Created by {@link WikiMetrics#startTimer(String, String...)}; closing the
|
||||
* sample records the elapsed nanoseconds against the named timer with the
|
||||
* given key-value tags. Closing twice is a no-op — safe inside try/finally
|
||||
* blocks that may also have an explicit {@code close()} call.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public class WikiTimerSample implements AutoCloseable {
|
||||
|
||||
private final WikiMetrics metrics;
|
||||
private final String name;
|
||||
private final Tags tags;
|
||||
private final long startNanos;
|
||||
private boolean closed;
|
||||
|
||||
WikiTimerSample(WikiMetrics metrics, String name, String[] keyValues, long startNanos) {
|
||||
this.metrics = metrics;
|
||||
this.name = name;
|
||||
Tags built = Tags.empty();
|
||||
for (int i = 0; i + 1 < keyValues.length; i += 2) {
|
||||
built = built.and(Tag.of(keyValues[i], keyValues[i + 1]));
|
||||
}
|
||||
this.tags = built;
|
||||
this.startNanos = startNanos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
metrics.recordTimer(name, tags, System.nanoTime() - startNanos);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user