Loki’s Table Manager doesn’t actually "handle" schema migrations; it enforces them by refusing to start if the schema version in your object store doesn’t match the version it expects.

This is Loki’s way of saying, "I can’t guarantee data integrity if the storage format has changed and I don’t know how to read it." The Table Manager is the gatekeeper for Loki’s historical data, ensuring that when Loki needs to access older data, it understands the structure it’s dealing with. It checks a specific metadata entry in your object store (usually S3 or GCS) against its compiled-in schema version. If they don’t align, it halts operations to prevent potential data corruption or unreadable data.

Let’s see the Table Manager in action. Imagine you have a Loki instance running and it’s been happily ingesting and querying data. The Table Manager has been doing its job in the background, periodically checking on the schema version of the data chunks stored in your object store.

Here’s a simplified view of how it might look in the code (this is illustrative, not the exact production code):

// Inside Table Manager's initialization
func (tm *TableManager) Start() {
    currentSchemaVersion, err := tm.metaStore.GetSchemaVersion()
    if err != nil {
        // Handle error: maybe schema version doesn't exist yet (first run)
        log.Warnf("Could not get schema version: %v", err)
        // Attempt to create it if it's the first time
        if !tm.metaStore.Exists() {
            log.Infof("Initializing schema version to %d", tm.schemaVersion)
            if err := tm.metaStore.SetSchemaVersion(tm.schemaVersion); err != nil {
                log.Fatalf("Failed to initialize schema version: %v", err)
            }
        }
    }

    if currentSchemaVersion != tm.schemaVersion {
        log.Fatalf("Schema version mismatch: expected %d, found %d. Please run schema migration.", tm.schemaVersion, currentSchemaVersion)
    }

    // ... proceed with other Table Manager tasks
}

The tm.metaStore.GetSchemaVersion() would be talking to your object store, looking for a specific key or object that holds the current schema version. If it finds it, it reads the integer value. If it doesn’t find it, it might mean this is the very first time Loki is initializing its metadata.

The tm.schemaVersion is the version Loki was compiled with. If these numbers don’t match, Loki refuses to proceed.

What problem does this solve?

Loki stores its index and chunk data in object storage. Over time, the format of this data can evolve. New features might require changes to how data is indexed or how chunks are compressed and stored. If Loki tried to read data written with an older schema using its newer code, it might fail to parse it, leading to query errors or data loss. The schema migration process ensures that all data in the object store is updated to a format that the current Loki version understands.

How does it work internally?

The Table Manager is responsible for more than just schema checks. It also manages the lifecycle of your time-series data by:

  1. Consolidating Chunks: Loki writes small chunks of data as they arrive. Over time, these small chunks can become inefficient. The Table Manager periodically consolidates these small chunks into larger ones, improving query performance and reducing the number of objects in your object store.
  2. Managing Schemas: This is where the migration aspect comes in. It checks the schema version and, if necessary, triggers or facilitates the migration process.
  3. Deleting Old Data: Based on your retention policies, the Table Manager will eventually delete old data. This involves identifying and removing the corresponding objects from your object store.

The Exact Levers You Control

You primarily interact with the Table Manager through Loki’s configuration. The most critical settings relate to its operational behavior:

  • common.path: This is the directory where Loki stores its operational data, including the schema version metadata. If you’re using object storage, this path is less about local disk and more about how Loki identifies its metadata within the object store.
  • table_manager.retention_enabled: A boolean (true or false) to enable or disable automatic data retention.
  • table_manager.retention_period: How long data should be kept (e.g., "168h" for 7 days).
  • table_manager.chunk_directory: The local filesystem path where chunks are temporarily stored before being uploaded to object storage. This is relevant if you’re not using direct-to-object-storage uploads.
  • table_manager.period: How often the Table Manager runs its tasks (e.g., "1h").

When Loki is configured to use object storage (like S3, GCS, Azure Blob Storage), the common.path often dictates a "folder" structure within that object store. For example, common.path: /loki/boltdb-shipper would mean Loki looks for its metadata and boltdb-shipper files in the loki/boltdb-shipper prefix in your bucket.

The schema version itself is typically stored as a single object or key within this common.path in your object store. For example, if common.path is loki/boltdb-shipper, the schema version might be stored in an object named loki/boltdb-shipper/schema_version.

When a schema migration is required, it’s usually because a new Loki release has a different schemaVersion compiled into it than what’s currently stored in your object store. You’d typically see an error message like:

level=error ts=... msg="schema version mismatch: expected 2, found 1. Please run schema migration."

The actual migration process is initiated by running a separate loki-migrator tool. This tool reads the old data, transforms it according to the new schema, and writes it back to object storage, finally updating the schema version metadata to the new value.

The most surprising truth about this is that the "schema version" isn’t a complex database schema; it’s just a single integer value stored in your object store. Loki’s Table Manager’s primary role in migrations is to detect the mismatch and prevent operations until the external loki-migrator tool has done its work. It’s a safeguard, not the migration engine itself.

The next concept you’ll likely encounter is managing the boltdb-shipper, which is Loki’s primary mechanism for managing its index in object storage.

Want structured learning?

Take the full Loki course →