Skip to main content
The sentry-options-validation library is the core validation engine that powers both the CLI and client libraries. It provides schema loading, JSON Schema validation, and automatic value reloading capabilities.

Overview

This library is used internally by:
  • CLI: Validates YAML files against schemas and generates JSON output
  • Rust client: Loads schemas and values, watches for file changes
  • Python client: Via PyO3 bindings to the Rust client
Key features:
  • Schema validation using JSON Schema
  • Thread-safe value storage with automatic reloading
  • Kubernetes-compatible naming validation
  • Efficient schema compilation and caching

Core types

SchemaRegistry

Loads and manages namespace schemas from disk.
schemas
HashMap<String, Arc<NamespaceSchema>>
Map of namespace names to compiled schemas

Methods

new()
SchemaRegistry
Creates an empty schema registry
from_directory(schemas_dir: &Path)
ValidationResult<SchemaRegistry>
Loads all schemas from a directory structure. Expects schemas/{namespace}/schema.json layout.Errors: Returns ValidationError if directory doesn’t exist or any schema is invalid
validate_values(namespace: &str, values: &Value)
ValidationResult<()>
Validates a complete values object against a namespace schemaParameters:
  • namespace: Namespace name
  • values: JSON object with option key-value pairs
Errors: Returns UnknownNamespace or ValueError with validation details
get(namespace: &str)
Option<&Arc<NamespaceSchema>>
Retrieves a compiled schema by namespace name
load_values_json(values_dir: &Path)
ValidationResult<(ValuesByNamespace, HashMap<String, String>)>
Loads and validates JSON values from directory. Expects {values_dir}/{namespace}/values.json structure.Returns: Tuple of (values map, generated_at timestamps by namespace)Note: Skips namespaces without values.json files

Usage example

use sentry_options_validation::SchemaRegistry;
use std::path::Path;

// Load schemas from directory
let registry = SchemaRegistry::from_directory(
    Path::new("/etc/sentry-options/schemas")
)?;

// Validate values for a namespace
let values = serde_json::json!({
    "my-option": "value",
    "another-option": 42
});
registry.validate_values("relay", &values)?;

// Get a specific schema
if let Some(schema) = registry.get("relay") {
    let default = schema.get_default("my-option");
}

NamespaceSchema

Compiled schema for a single namespace with validation and metadata.
namespace
String
Namespace identifier
options
HashMap<String, OptionMetadata>
Metadata for each option defined in the schema
validator
jsonschema::Validator
Compiled JSON Schema validator (private)

Methods

validate_values(values: &Value)
ValidationResult<()>
Validates values object against this namespace schemaErrors: Returns ValueError with detailed validation errors including field paths
get_default(key: &str)
Option<&Value>
Returns the default value for an option key, or None if key doesn’t exist

OptionMetadata

Metadata extracted from a schema property definition.
option_type
String
JSON Schema type (“string”, “integer”, “number”, “boolean”, “array”, “object”)
property_schema
Value
Complete property schema definition including type, default, description
default
Value
Default value for the option

ValuesWatcher

Background thread that polls for file changes and reloads values automatically.
stop_signal
Arc<AtomicBool>
Signal to stop the watcher thread
thread
Option<JoinHandle<()>>
Handle to the background polling thread

Methods

new(values_path: &Path, registry: Arc<SchemaRegistry>, values: Arc<RwLock<ValuesByNamespace>>)
ValidationResult<ValuesWatcher>
Creates and starts a background watcher threadParameters:
  • values_path: Directory containing namespace values.json files
  • registry: Schema registry for validation
  • values: Shared values map that will be updated on changes
Note: Prints warning if directory doesn’t exist (unless SENTRY_OPTIONS_SUPPRESS_MISSING_DIR=1)
stop(&mut self)
()
Stops the watcher thread and waits for it to join (may take up to 5 seconds)
is_alive()
bool
Returns whether the watcher thread is still running

Behavior

  • Polls every 5 seconds for file modifications
  • Tracks most recent mtime across all values.json files
  • On change: validates all namespaces, updates values atomically
  • If validation fails: keeps old values for all namespaces
  • Emits Sentry transactions with reload metrics and propagation delay
  • Thread automatically stops when ValuesWatcher is dropped

Usage example

use sentry_options_validation::{SchemaRegistry, ValuesWatcher};
use std::sync::{Arc, RwLock};
use std::path::Path;

let registry = Arc::new(SchemaRegistry::from_directory(
    Path::new("./schemas")
)?);

let (initial_values, _) = registry.load_values_json(
    Path::new("./values")
)?;
let values = Arc::new(RwLock::new(initial_values));

// Start watching for changes
let mut watcher = ValuesWatcher::new(
    Path::new("./values"),
    Arc::clone(&registry),
    Arc::clone(&values)
)?;

// Read values (automatically updated in background)
{
    let guard = values.read().unwrap();
    if let Some(ns_values) = guard.get("relay") {
        println!("Option: {:?}", ns_values.get("my-option"));
    }
}

// Stop watcher when done
watcher.stop();

Validation errors

The ValidationError enum covers all validation failure cases:
SchemaError
{ file: PathBuf, message: String }
Schema file is invalid or doesn’t match meta-schema
ValueError
{ namespace: String, errors: String }
Values don’t match schema. Error string contains field paths and descriptions.
UnknownNamespace
String
Referenced namespace doesn’t exist in registry
InternalError
String
Internal library error (meta-schema compilation failure, etc.)
FileRead
std::io::Error
File system error reading schema or values
JSONParse
serde_json::Error
Invalid JSON in schema or values file
ValidationErrors
Vec<ValidationError>
Multiple validation errors bundled together
InvalidName
{ label: String, name: String, reason: String }
Invalid Kubernetes-style name (must be lowercase alphanumeric with - or .)

Error formatting

match registry.validate_values("relay", &values) {
    Ok(_) => println!("Valid!"),
    Err(ValidationError::ValueError { namespace, errors }) => {
        eprintln!("Validation failed for {}: {}", namespace, errors);
        // Example output:
        // Validation failed for relay:
        //   my-option "value" is not of type "integer"
        //   unknown-key Additional properties are not allowed
    }
    Err(e) => eprintln!("Error: {}", e),
}

Helper functions

resolve_options_dir()

resolve_options_dir()
PathBuf
Resolves the options directory using fallback chain:
  1. SENTRY_OPTIONS_DIR environment variable
  2. /etc/sentry-options (if exists)
  3. sentry-options/ (local fallback)
use sentry_options_validation::resolve_options_dir;

let options_dir = resolve_options_dir();
let schemas_dir = options_dir.join("schemas");
let values_dir = options_dir.join("values");

validate_k8s_name_component()

validate_k8s_name_component(name: &str, label: &str)
ValidationResult<()>
Validates that a name follows Kubernetes naming rules:
  • Lowercase letters, digits, -, . only
  • Must start and end with alphanumeric
Used internally to validate namespace and target names.
use sentry_options_validation::validate_k8s_name_component;

// Valid names
validate_k8s_name_component("relay", "namespace")?;
validate_k8s_name_component("my-service", "namespace")?;
validate_k8s_name_component("v1.2.3", "namespace")?;

// Invalid - returns InvalidName error
validate_k8s_name_component("MyService", "namespace")?;  // uppercase
validate_k8s_name_component("my_service", "namespace")?; // underscore
validate_k8s_name_component("-service", "namespace")?;   // leading hyphen

Constants

PRODUCTION_OPTIONS_DIR
&str
/etc/sentry-options - Production path where ConfigMaps are mounted
LOCAL_OPTIONS_DIR
&str
sentry-options - Local development fallback path
OPTIONS_DIR_ENV
&str
SENTRY_OPTIONS_DIR - Environment variable to override options directory
OPTIONS_SUPPRESS_MISSING_DIR_ENV
&str
SENTRY_OPTIONS_SUPPRESS_MISSING_DIR - Set to 1 or true to suppress missing directory warnings

Type aliases

ValidationResult<T>
Result<T, ValidationError>
Standard result type for validation operations
ValuesByNamespace
HashMap<String, HashMap<String, Value>>
Map structure: namespace -> (option_key -> value)

Internal usage by CLI

The CLI uses SchemaRegistry to validate YAML files and merge them into JSON output:
use sentry_options_validation::SchemaRegistry;
use std::path::Path;

// Load schemas
let registry = SchemaRegistry::from_directory(
    Path::new("./schemas")
)?;

// Load and validate YAML files (custom loader)
let namespace_values = load_yaml_files("./values")?;

// Validate each namespace
for (namespace, values) in &namespace_values {
    registry.validate_values(namespace, values)?;
}

// Generate JSON output files
write_json_files(namespace_values, "./output")?;

Internal usage by Rust client

The Rust client uses SchemaRegistry + ValuesWatcher for runtime value access:
use sentry_options_validation::{
    SchemaRegistry, ValuesWatcher, resolve_options_dir
};
use std::sync::{Arc, RwLock};

pub struct Options {
    registry: Arc<SchemaRegistry>,
    values: Arc<RwLock<ValuesByNamespace>>,
    _watcher: ValuesWatcher,
}

impl Options {
    pub fn new() -> Result<Self> {
        let dir = resolve_options_dir();
        let schemas_dir = dir.join("schemas");
        let values_dir = dir.join("values");
        
        let registry = Arc::new(
            SchemaRegistry::from_directory(&schemas_dir)?
        );
        let (loaded, _) = registry.load_values_json(&values_dir)?;
        let values = Arc::new(RwLock::new(loaded));
        
        let watcher = ValuesWatcher::new(
            &values_dir,
            Arc::clone(&registry),
            Arc::clone(&values)
        )?;
        
        Ok(Self { registry, values, _watcher: watcher })
    }
    
    pub fn get(&self, namespace: &str, key: &str) -> Result<Value> {
        let schema = self.registry.get(namespace)
            .ok_or(OptionsError::UnknownNamespace)?;
        
        let guard = self.values.read().unwrap();
        if let Some(value) = guard.get(namespace)
            .and_then(|ns| ns.get(key))
        {
            return Ok(value.clone());
        }
        
        // Fall back to schema default
        schema.get_default(key)
            .cloned()
            .ok_or(OptionsError::UnknownOption)
    }
}

Observability

The ValuesWatcher emits Sentry transactions on each reload with:
transaction.op
string
sentry_options.reload
transaction.name
string
Namespace name (e.g., relay)
reload_duration_ms
number
Time taken to load and validate values in milliseconds
applied_at
string
RFC3339 timestamp when values were applied
generated_at
string
RFC3339 timestamp from values.json (if present)
propagation_delay_secs
number
Time between generation and application in seconds (if generated_at available)
These metrics use a dedicated Sentry DSN isolated from the host application.

Thread safety

  • SchemaRegistry uses Arc<NamespaceSchema> for efficient sharing
  • ValuesWatcher uses Arc<RwLock<ValuesByNamespace>> for concurrent reads
  • Writer may starve under heavy read load (standard RwLock behavior)
  • Watcher thread automatically stops when ValuesWatcher is dropped

Validation logic

  1. Schema validation (at load time):
    • Schema must match namespace-schema.json meta-schema
    • Must have version field
    • All properties require type, default, description
    • Default values must match their declared type
    • Namespace names must be valid Kubernetes identifiers
  2. Value validation (at runtime):
    • Values validated against compiled JSON Schema
    • Unknown options rejected (additionalProperties: false injected)
    • Type mismatches reported with field paths
    • All errors collected and reported together

Dependencies

Key external crates:
  • jsonschema - JSON Schema Draft 7 validation
  • serde_json - JSON parsing and manipulation
  • sentry - Observability and error tracking
  • thiserror - Error type definitions

Build docs developers (and LLMs) love