Common classes used across Spring AI providing document processing, text transformation, embedding utilities, observability support, and tokenization capabilities for AI application development
Observability provides comprehensive monitoring and tracing capabilities for AI operations using OpenTelemetry conventions.
The observability layer consists of:
These components enable telemetry, monitoring, and debugging of AI applications using industry-standard OpenTelemetry semantics.
Metadata record for AI operations (inference, fine-tuning, evaluation).
package org.springframework.ai.observation;
record AiOperationMetadata(String operationType, String provider) {
/**
* Create AiOperationMetadata.
* @param operationType type of operation (e.g., "chat", "embedding")
* @param provider AI provider name (e.g., "openai", "anthropic")
*/
AiOperationMetadata(String operationType, String provider);
/**
* Create builder.
* @return builder instance
*/
static Builder builder();
}class AiOperationMetadata.Builder {
/**
* Set operation type.
* @param operationType operation type
* @return this builder
*/
Builder operationType(String operationType);
/**
* Set provider name.
* @param provider provider name
* @return this builder
*/
Builder provider(String provider);
/**
* Build metadata record.
* @return AiOperationMetadata
*/
AiOperationMetadata build();
}import org.springframework.ai.observation.AiOperationMetadata;
// Create metadata directly
AiOperationMetadata metadata1 = new AiOperationMetadata("chat", "openai");
// Create with builder
AiOperationMetadata metadata2 = AiOperationMetadata.builder()
.operationType("embedding")
.provider("anthropic")
.build();
// Access metadata
String opType = metadata2.operationType();
String provider = metadata2.provider();
System.out.println("Operation: " + opType + ", Provider: " + provider);Utility methods for observability operations.
package org.springframework.ai.observation;
import java.util.List;
import java.util.Map;
abstract class ObservabilityHelper {
/**
* Concatenate map entries to JSON-like string.
* Converts key-value pairs to "key1:value1,key2:value2" format.
* @param keyValues map to concatenate
* @return concatenated string
*/
static String concatenateEntries(Map<String, Object> keyValues);
/**
* Concatenate strings to JSON array-like string.
* Converts list to "[item1, item2, item3]" format.
* @param strings list of strings
* @return concatenated string
*/
static String concatenateStrings(List<String> strings);
}import org.springframework.ai.observation.ObservabilityHelper;
import java.util.List;
import java.util.Map;
// Concatenate map entries
Map<String, Object> attributes = Map.of(
"model", "gpt-4",
"temperature", 0.7,
"max_tokens", 1000
);
String attributesStr = ObservabilityHelper.concatenateEntries(attributes);
System.out.println(attributesStr);
// Output: "model:gpt-4,temperature:0.7,max_tokens:1000"
// Concatenate strings
List<String> finishReasons = List.of("stop", "length", "content_filter");
String reasonsStr = ObservabilityHelper.concatenateStrings(finishReasons);
System.out.println(reasonsStr);
// Output: "[stop, length, content_filter]"
// Use in logging
Map<String, Object> metadata = Map.of(
"provider", "openai",
"operation", "chat",
"tokens_used", 500
);
String logMessage = "AI Operation: " + ObservabilityHelper.concatenateEntries(metadata);
System.out.println(logMessage);Wraps ObservationHandler to make tracing data available for logging.
package org.springframework.ai.observation;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.tracing.Tracer;
class TracingAwareLoggingObservationHandler<T extends Observation.Context>
implements ObservationHandler<T> {
/**
* Create handler with delegate and tracer.
* @param delegate delegate observation handler
* @param tracer tracer for tracing context
*/
TracingAwareLoggingObservationHandler(ObservationHandler<T> delegate, Tracer tracer);
/**
* Handle observation start.
* @param context observation context
*/
void onStart(T context);
/**
* Handle observation error.
* @param context observation context
*/
void onError(T context);
/**
* Handle observation event.
* @param event observation event
* @param context observation context
*/
void onEvent(Observation.Event event, T context);
/**
* Handle scope opened.
* @param context observation context
*/
void onScopeOpened(T context);
/**
* Handle scope closed.
* @param context observation context
*/
void onScopeClosed(T context);
/**
* Handle scope reset.
* @param context observation context
*/
void onScopeReset(T context);
/**
* Handle observation stop with tracing context.
* @param context observation context
*/
void onStop(T context);
/**
* Check if handler supports context.
* @param context observation context
* @return true if supported
*/
boolean supportsContext(Observation.Context context);
}import org.springframework.ai.observation.TracingAwareLoggingObservationHandler;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.tracing.Tracer;
// Assuming you have a delegate handler and tracer
ObservationHandler<MyContext> delegateHandler = // ... your handler
Tracer tracer = // ... your tracer
// Wrap handler to add tracing awareness
TracingAwareLoggingObservationHandler<MyContext> tracingHandler =
new TracingAwareLoggingObservationHandler<>(delegateHandler, tracer);
// Register with ObservationRegistry
// observationRegistry.observationConfig().observationHandler(tracingHandler);OpenTelemetry attribute keys for AI operations following Gen AI semantic conventions.
package org.springframework.ai.observation.conventions;
enum AiObservationAttributes {
// Operation attributes
AI_OPERATION_TYPE("gen_ai.operation.name"),
AI_PROVIDER("gen_ai.system"),
// Request attributes
REQUEST_MODEL("gen_ai.request.model"),
REQUEST_FREQUENCY_PENALTY("gen_ai.request.frequency_penalty"),
REQUEST_MAX_TOKENS("gen_ai.request.max_tokens"),
REQUEST_PRESENCE_PENALTY("gen_ai.request.presence_penalty"),
REQUEST_STOP_SEQUENCES("gen_ai.request.stop_sequences"),
REQUEST_TEMPERATURE("gen_ai.request.temperature"),
REQUEST_TOOL_NAMES("spring.ai.model.request.tool.names"),
REQUEST_TOP_K("gen_ai.request.top_k"),
REQUEST_TOP_P("gen_ai.request.top_p"),
REQUEST_EMBEDDING_DIMENSIONS("gen_ai.request.embedding.dimensions"),
REQUEST_IMAGE_RESPONSE_FORMAT("gen_ai.request.image.response_format"),
REQUEST_IMAGE_SIZE("gen_ai.request.image.size"),
REQUEST_IMAGE_STYLE("gen_ai.request.image.style"),
// Response attributes
RESPONSE_FINISH_REASONS("gen_ai.response.finish_reasons"),
RESPONSE_ID("gen_ai.response.id"),
RESPONSE_MODEL("gen_ai.response.model"),
// Usage attributes
USAGE_INPUT_TOKENS("gen_ai.usage.input_tokens"),
USAGE_OUTPUT_TOKENS("gen_ai.usage.output_tokens"),
USAGE_TOTAL_TOKENS("gen_ai.usage.total_tokens");
/**
* Get attribute key.
* @return attribute key string
*/
String value();
}Metric-specific attributes.
package org.springframework.ai.observation.conventions;
enum AiObservationMetricAttributes {
TOKEN_TYPE("gen_ai.token.type");
String value();
}Standard metric names for AI operations.
package org.springframework.ai.observation.conventions;
enum AiObservationMetricNames {
OPERATION_DURATION("gen_ai.client.operation.duration"),
TOKEN_USAGE("gen_ai.client.token.usage");
String value();
}Types of AI operations.
package org.springframework.ai.observation.conventions;
enum AiOperationType {
CHAT("chat"),
EMBEDDING("embedding"),
FRAMEWORK("framework"),
IMAGE("image"),
TEXT_COMPLETION("text_completion");
String value();
}AI system providers.
package org.springframework.ai.observation.conventions;
enum AiProvider {
ANTHROPIC("anthropic"),
AZURE_OPENAI("azure-openai"),
BEDROCK_CONVERSE("bedrock_converse"),
DEEPSEEK("deepseek"),
GOOGLE_GENAI_AI("google_genai"),
MINIMAX("minimax"),
MISTRAL_AI("mistral_ai"),
OCI_GENAI("oci_genai"),
OLLAMA("ollama"),
ONNX("onnx"),
OPENAI("openai"),
OPENAI_SDK("openai_sdk"),
SPRING_AI("spring_ai"),
VERTEX_AI("vertex_ai"),
ZHIPUAI("zhipuai");
String value();
}Token types for usage metrics.
package org.springframework.ai.observation.conventions;
enum AiTokenType {
INPUT("input"),
OUTPUT("output"),
TOTAL("total");
String value();
}Types of Spring AI constructs.
package org.springframework.ai.observation.conventions;
enum SpringAiKind {
ADVISOR("advisor"),
CHAT_CLIENT("chat_client"),
TOOL_CALL("tool_call"),
VECTOR_STORE("vector_store");
String value();
}OpenTelemetry attribute keys for vector store operations.
package org.springframework.ai.observation.conventions;
enum VectorStoreObservationAttributes {
DB_COLLECTION_NAME("db.collection.name"),
DB_NAMESPACE("db.namespace"),
DB_OPERATION_NAME("db.operation.name"),
DB_RECORD_ID("db.record.id"),
DB_SYSTEM("db.system"),
DB_SEARCH_SIMILARITY_METRIC("db.search.similarity_metric"),
DB_VECTOR_DIMENSION_COUNT("db.vector.dimension_count"),
DB_VECTOR_FIELD_NAME("db.vector.field_name"),
DB_VECTOR_QUERY_CONTENT("db.vector.query.content"),
DB_VECTOR_QUERY_FILTER("db.vector.query.filter"),
DB_VECTOR_QUERY_RESPONSE_DOCUMENTS("db.vector.query.response.documents"),
DB_VECTOR_QUERY_SIMILARITY_THRESHOLD("db.vector.query.similarity_threshold"),
DB_VECTOR_QUERY_TOP_K("db.vector.query.top_k");
String value();
}Vector store systems.
package org.springframework.ai.observation.conventions;
enum VectorStoreProvider {
AZURE("azure"),
CASSANDRA("cassandra"),
CHROMA("chroma"),
COSMOSDB("cosmosdb"),
COUCHBASE("couchbase"),
ELASTICSEARCH("elasticsearch"),
GEMFIRE("gemfire"),
HANA("hana"),
MARIADB("mariadb"),
MILVUS("milvus"),
MONGODB("mongodb"),
NEO4J("neo4j"),
OPENSEARCH("opensearch"),
ORACLE("oracle"),
PG_VECTOR("pgvector"),
PINECONE("pinecone"),
QDRANT("qdrant"),
REDIS("redis"),
SIMPLE("simple"),
TYPESENSE("typesense"),
WEAVIATE("weaviate");
String value();
}Similarity metrics for vector search.
package org.springframework.ai.observation.conventions;
enum VectorStoreSimilarityMetric {
COSINE("cosine"),
DOT("dot"),
EUCLIDEAN("euclidean"),
MANHATTAN("manhattan");
String value();
}import org.springframework.ai.observation.conventions.*;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
/**
* Record metrics for AI operations.
*/
class AIOperationMetricsRecorder {
private final ObservationRegistry observationRegistry;
public AIOperationMetricsRecorder(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
public void recordChatOperation(String model, int inputTokens, int outputTokens,
String finishReason) {
Observation observation = Observation.createNotStarted(
"ai.chat.operation",
observationRegistry
);
observation
.lowCardinalityKeyValue(
AiObservationAttributes.AI_OPERATION_TYPE.value(),
AiOperationType.CHAT.value()
)
.lowCardinalityKeyValue(
AiObservationAttributes.AI_PROVIDER.value(),
AiProvider.OPENAI.value()
)
.lowCardinalityKeyValue(
AiObservationAttributes.REQUEST_MODEL.value(),
model
)
.highCardinalityKeyValue(
AiObservationAttributes.USAGE_INPUT_TOKENS.value(),
String.valueOf(inputTokens)
)
.highCardinalityKeyValue(
AiObservationAttributes.USAGE_OUTPUT_TOKENS.value(),
String.valueOf(outputTokens)
)
.highCardinalityKeyValue(
AiObservationAttributes.RESPONSE_FINISH_REASONS.value(),
finishReason
);
observation.observe(() -> {
// Actual operation happens here
System.out.println("Executing chat operation...");
});
}
public void recordEmbeddingOperation(String model, int tokenCount, int dimensions) {
Observation.createNotStarted("ai.embedding.operation", observationRegistry)
.lowCardinalityKeyValue(
AiObservationAttributes.AI_OPERATION_TYPE.value(),
AiOperationType.EMBEDDING.value()
)
.lowCardinalityKeyValue(
AiObservationAttributes.REQUEST_MODEL.value(),
model
)
.highCardinalityKeyValue(
AiObservationAttributes.USAGE_INPUT_TOKENS.value(),
String.valueOf(tokenCount)
)
.observe(() -> {
System.out.println("Executing embedding operation...");
});
}
}
// Usage
ObservationRegistry registry = // ... your registry
AIOperationMetricsRecorder recorder = new AIOperationMetricsRecorder(registry);
recorder.recordChatOperation("gpt-4", 150, 450, "stop");
recorder.recordEmbeddingOperation("text-embedding-3-small", 500, 1536);import org.springframework.ai.observation.conventions.*;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
/**
* Track vector store operations.
*/
class VectorStoreObserver {
private final ObservationRegistry observationRegistry;
public VectorStoreObserver(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
public void recordSearch(String collection, String query, int topK,
String similarityMetric, int dimensions) {
Observation.createNotStarted("vector.store.search", observationRegistry)
.lowCardinalityKeyValue(
VectorStoreObservationAttributes.DB_SYSTEM.value(),
VectorStoreProvider.PG_VECTOR.value()
)
.lowCardinalityKeyValue(
VectorStoreObservationAttributes.DB_OPERATION_NAME.value(),
"search"
)
.lowCardinalityKeyValue(
VectorStoreObservationAttributes.DB_COLLECTION_NAME.value(),
collection
)
.lowCardinalityKeyValue(
VectorStoreObservationAttributes.DB_SEARCH_SIMILARITY_METRIC.value(),
similarityMetric
)
.highCardinalityKeyValue(
VectorStoreObservationAttributes.DB_VECTOR_QUERY_TOP_K.value(),
String.valueOf(topK)
)
.highCardinalityKeyValue(
VectorStoreObservationAttributes.DB_VECTOR_DIMENSION_COUNT.value(),
String.valueOf(dimensions)
)
.observe(() -> {
System.out.println("Executing vector search...");
});
}
public void recordAdd(String collection, int documentCount, int dimensions) {
Observation.createNotStarted("vector.store.add", observationRegistry)
.lowCardinalityKeyValue(
VectorStoreObservationAttributes.DB_SYSTEM.value(),
VectorStoreProvider.CHROMA.value()
)
.lowCardinalityKeyValue(
VectorStoreObservationAttributes.DB_OPERATION_NAME.value(),
"add"
)
.lowCardinalityKeyValue(
VectorStoreObservationAttributes.DB_COLLECTION_NAME.value(),
collection
)
.highCardinalityKeyValue(
VectorStoreObservationAttributes.DB_VECTOR_DIMENSION_COUNT.value(),
String.valueOf(dimensions)
)
.highCardinalityKeyValue(
"document_count",
String.valueOf(documentCount)
)
.observe(() -> {
System.out.println("Adding documents to vector store...");
});
}
}
// Usage
ObservationRegistry registry = // ... your registry
VectorStoreObserver observer = new VectorStoreObserver(registry);
observer.recordSearch(
"knowledge_base",
"What is Spring AI?",
5,
VectorStoreSimilarityMetric.COSINE.value(),
1536
);
observer.recordAdd("knowledge_base", 100, 1536);import org.springframework.ai.observation.conventions.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Collect and aggregate AI operation metrics.
*/
class AIMetricsDashboard {
private final Map<String, OperationMetrics> operationMetrics = new ConcurrentHashMap<>();
public void recordOperation(String operationType, String provider,
long durationMs, int inputTokens, int outputTokens) {
String key = operationType + ":" + provider;
operationMetrics.computeIfAbsent(key, k -> new OperationMetrics())
.record(durationMs, inputTokens, outputTokens);
}
public DashboardSnapshot getSnapshot() {
Map<String, OperationStats> stats = new ConcurrentHashMap<>();
operationMetrics.forEach((key, metrics) -> {
stats.put(key, metrics.getStats());
});
return new DashboardSnapshot(stats);
}
static class OperationMetrics {
private final AtomicInteger requestCount = new AtomicInteger(0);
private final AtomicLong totalDurationMs = new AtomicLong(0);
private final AtomicLong totalInputTokens = new AtomicLong(0);
private final AtomicLong totalOutputTokens = new AtomicLong(0);
void record(long durationMs, int inputTokens, int outputTokens) {
requestCount.incrementAndGet();
totalDurationMs.addAndGet(durationMs);
totalInputTokens.addAndGet(inputTokens);
totalOutputTokens.addAndGet(outputTokens);
}
OperationStats getStats() {
int count = requestCount.get();
if (count == 0) return new OperationStats(0, 0, 0, 0, 0);
return new OperationStats(
count,
totalDurationMs.get() / count,
totalInputTokens.get(),
totalOutputTokens.get(),
(totalInputTokens.get() + totalOutputTokens.get()) / count
);
}
}
record OperationStats(
int requestCount,
long avgDurationMs,
long totalInputTokens,
long totalOutputTokens,
long avgTokensPerRequest
) {}
record DashboardSnapshot(Map<String, OperationStats> operationStats) {
public String formatReport() {
StringBuilder report = new StringBuilder("AI Operations Dashboard\n");
report.append("=".repeat(60)).append("\n\n");
operationStats.forEach((key, stats) -> {
report.append(String.format("""
Operation: %s
- Requests: %,d
- Avg Duration: %,d ms
- Total Input Tokens: %,d
- Total Output Tokens: %,d
- Avg Tokens/Request: %,d
""",
key,
stats.requestCount(),
stats.avgDurationMs(),
stats.totalInputTokens(),
stats.totalOutputTokens(),
stats.avgTokensPerRequest()
));
});
return report.toString();
}
}
}
// Usage
AIMetricsDashboard dashboard = new AIMetricsDashboard();
// Record operations
dashboard.recordOperation(
AiOperationType.CHAT.value(),
AiProvider.OPENAI.value(),
1200, // 1.2 seconds
150, // input tokens
450 // output tokens
);
dashboard.recordOperation(
AiOperationType.EMBEDDING.value(),
AiProvider.OPENAI.value(),
300, // 0.3 seconds
500, // input tokens
0 // no output tokens for embedding
);
// Get snapshot
AIMetricsDashboard.DashboardSnapshot snapshot = dashboard.getSnapshot();
System.out.println(snapshot.formatReport());import org.springframework.ai.observation.conventions.*;
import org.springframework.ai.observation.ObservabilityHelper;
import java.util.Map;
import java.util.List;
/**
* Examples of using standardized observation attributes.
*/
class StandardizedObservability {
public void logChatRequest(String model, double temperature, int maxTokens,
int topK, double topP) {
Map<String, Object> attributes = Map.of(
AiObservationAttributes.REQUEST_MODEL.value(), model,
AiObservationAttributes.REQUEST_TEMPERATURE.value(), temperature,
AiObservationAttributes.REQUEST_MAX_TOKENS.value(), maxTokens,
AiObservationAttributes.REQUEST_TOP_K.value(), topK,
AiObservationAttributes.REQUEST_TOP_P.value(), topP
);
String attributesStr = ObservabilityHelper.concatenateEntries(attributes);
System.out.println("Chat Request Attributes: " + attributesStr);
}
public void logChatResponse(String model, List<String> finishReasons,
int inputTokens, int outputTokens) {
Map<String, Object> responseAttrs = Map.of(
AiObservationAttributes.RESPONSE_MODEL.value(), model,
AiObservationAttributes.USAGE_INPUT_TOKENS.value(), inputTokens,
AiObservationAttributes.USAGE_OUTPUT_TOKENS.value(), outputTokens,
AiObservationAttributes.USAGE_TOTAL_TOKENS.value(), inputTokens + outputTokens
);
String finishReasonsStr = ObservabilityHelper.concatenateStrings(finishReasons);
System.out.println("Response Attributes: " +
ObservabilityHelper.concatenateEntries(responseAttrs));
System.out.println("Finish Reasons: " + finishReasonsStr);
}
public void logVectorStoreQuery(String collection, int topK, String metric,
int dimensions) {
Map<String, Object> queryAttrs = Map.of(
VectorStoreObservationAttributes.DB_COLLECTION_NAME.value(), collection,
VectorStoreObservationAttributes.DB_VECTOR_QUERY_TOP_K.value(), topK,
VectorStoreObservationAttributes.DB_SEARCH_SIMILARITY_METRIC.value(), metric,
VectorStoreObservationAttributes.DB_VECTOR_DIMENSION_COUNT.value(), dimensions
);
System.out.println("Vector Query: " +
ObservabilityHelper.concatenateEntries(queryAttrs));
}
}
// Usage
StandardizedObservability obs = new StandardizedObservability();
obs.logChatRequest("gpt-4", 0.7, 1000, 10, 0.9);
obs.logChatResponse("gpt-4", List.of("stop"), 150, 450);
obs.logVectorStoreQuery("knowledge_base", 5, "cosine", 1536);Thread Safety:
AiOperationMetadata: Immutable record, thread-safeObservabilityHelper: Thread-safe (static utility methods)TracingAwareLoggingObservationHandler: Thread-safe if delegate handler is thread-safePerformance:
Common Exceptions:
NullPointerException: If required observation parameters are nullIllegalArgumentException: If observation context is invalidRuntimeException: Observation handler errors (depends on implementation)Edge Cases:
// Empty metadata map
Map<String, Object> empty = Map.of();
String result = ObservabilityHelper.concatenateEntries(empty); // Returns ""
// Null values in map
Map<String, Object> withNull = Map.of("key", "value");
// Note: Map.of() doesn't allow null values, will throw NullPointerException
// Empty list
List<String> empty = List.of();
String result = ObservabilityHelper.concatenateStrings(empty); // Returns "[]"
// Observation with null context
try {
Observation observation = Observation.createNotStarted("name", registry);
observation.observe(() -> {
// operation
});
} catch (Exception e) {
// Handle observation errors
}Semantic Conventions: Spring AI Commons follows OpenTelemetry Gen AI semantic conventions for interoperability:
gen_ai.* prefix for AI operationsgen_ai.client.* prefixdb.* prefix.value() method for string representationAttribute Cardinality:
Example:
observation
.lowCardinalityKeyValue("gen_ai.system", "openai") // Low: few distinct values
.highCardinalityKeyValue("gen_ai.response.id", responseId); // High: many distinct valuesMicrometer Configuration:
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.aop.ObservedAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class ObservabilityConfig {
@Bean
ObservationRegistry observationRegistry() {
return ObservationRegistry.create();
}
@Bean
ObservedAspect observedAspect(ObservationRegistry registry) {
return new ObservedAspect(registry);
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-springframework-ai--spring-ai-commons