# Reference

# Grafana Loki configuration parameters

Grafana Loki is configured in a YAML file (usually referred to as `loki.yaml` )
which contains information on the Loki server and its individual components,
depending on which mode Loki is launched in.

Configuration examples can be found in the [Configuration Examples](https://grafana.com/docs/loki/<LOKI_VERSION>/configure/examples/configuration-examples/) document.

<!-- The shared `configuration.md` file is generated from `/docs/templates/configuration.template`. To make changes to the included content, modify the template file and run `make doc` from root directory to regenerate the shared file. -->

{{< docs/shared lookup="configuration.md" source="loki" version="<LOKI_VERSION>" >}}

# Loki HTTP API

Loki exposes an HTTP API for pushing, querying, and tailing log data, as well
as for viewing and managing cluster information.

## Authentication

Authentication and authorization are not part of the Loki API itself. How you authenticate depends on how Loki is deployed:

- **Self-hosted, single-tenant Loki**: Loki doesn't enforce authentication. If you need to restrict access, put a reverse proxy (such as NGINX or Envoy) in front of Loki to handle authentication separately.
- **Self-hosted, multi-tenant Loki**: Set the `X-Scope-OrgID` header on each request to identify the tenant. For details and examples, refer to [Multi-tenant queries](#multi-tenant-queries).
- **Grafana Enterprise Logs (GEL)**: Use HTTP Basic Authentication, with the tenant name as the username and an access policy token as the password. For an example, refer to [Multi-tenant queries](#multi-tenant-queries).
- **Grafana Cloud**: Use HTTP Basic Authentication, with your Loki instance ID (the **User** value from the Cloud Portal) as the username and a [Cloud Access Policy token](https://grafana.com/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-policies/) as the password:

  ```bash
  curl -u "<LOKI_INSTANCE_ID>:<CLOUD_ACCESS_POLICY_TOKEN>" \
    -G -s "<LOKI_URL>/loki/api/v1/query" \
    --data-urlencode 'query=sum(rate({job="varlogs"}[10m])) by (level)' | jq
  ```

  For step-by-step instructions on finding your Loki instance ID, URL, and creating an access policy token, refer to [Authenticate to query Cloud Logs](https://grafana.com/docs/grafana-cloud/send-data/logs/authenticate-to-query-logs/) in the Grafana Cloud documentation.

## Endpoints

### Ingest endpoints

These endpoints are exposed by the `distributor`, `write`, and `all` components:

- [`POST /loki/api/v1/push`](#ingest-logs)
- [`POST /otlp/v1/logs`](#ingest-logs-using-otlp)

A [list of clients](../../send-data/) can be found in the clients documentation.

### Query endpoints

{{< admonition type="note" >}}
Requests sent to the query endpoints must use valid LogQL syntax. For more information, see the [LogQL](../../query/) section of the documentation.
{{< /admonition >}}

{{< admonition type="tip" >}}
For Python examples of these endpoints, see [Query Loki with Python](../python-client-examples/).
{{< /admonition >}}

These HTTP endpoints are exposed by the `querier`, `query-frontend`, `read`, and `all` components:

- [`GET /loki/api/v1/query`](#query-logs-at-a-single-point-in-time)
- [`GET /loki/api/v1/query_range`](#query-logs-within-a-range-of-time)
- [`GET /loki/api/v1/labels`](#query-labels)
- [`GET /loki/api/v1/label/<name>/values`](#query-label-values)
- [`GET /loki/api/v1/series`](#query-streams)
- [`GET /loki/api/v1/index/stats`](#query-log-statistics)
- [`GET /loki/api/v1/index/volume`](#query-log-volume)
- [`GET /loki/api/v1/index/volume_range`](#query-log-volume)
- [`GET /loki/api/v1/patterns`](#patterns-detection)
- [`GET /loki/api/v1/tail`](#stream-logs)

### Status endpoints

These HTTP endpoints are exposed by all components and return the status of the component:

- [`GET /ready`](#readiness-probe)
- [`GET /log_level`](#change-log-level)
- [`GET /metrics`](#prometheus-metrics)
- [`GET /config`](#show-current-configuration)
- [`GET /loki/api/v1/exposed-config`](#show-exposed-configuration)
- [`GET /services`](#list-running-services)
- [`GET /loki/api/v1/status/buildinfo`](#show-build-information)

### Ring endpoints

These HTTP endpoints are exposed by their respective component that is part of the ring URL prefix:

- [`GET /distributor/ring`](#distributor-ring-status)
- [`GET /indexgateway/ring`](#index-gateway-ring-status)
- [`GET /ruler/ring`](#ruler-ring-status)
- [`GET /compactor/ring`](#compactor-ring-status)

### Flush/shutdown endpoints

These HTTP endpoints are exposed by the `ingester`, `write`, and `all` components for flushing chunks and/or shutting down.

- [`POST /flush`](#flush-in-memory-chunks-to-backing-store)
- [`POST /flush/tenant`](#flush-in-memory-chunks-for-a-tenant-experimental)
- [`POST /ingester/prepare_shutdown`](#prepare-ingester-shutdown)
- [`POST /ingester/shutdown`](#flush-in-memory-chunks-and-shut-down)

### Index gateway endpoints

These HTTP endpoints are exposed by the `index-gateway`, `backend`, and `all` components:

- [`PUT /sync-indexes`](#sync-indexes-from-object-storage)
- [`GET /sync-indexes`](#sync-indexes-from-object-storage)

### Rule endpoints

These HTTP endpoints are exposed by the `ruler` component:

- [`GET /loki/api/v1/rules`](#list-rule-groups)
- [`GET /loki/api/v1/rules/{namespace}`](#get-rule-groups-by-namespace)
- [`GET /loki/api/v1/rules/{namespace}/{groupName}`](#get-rule-group)
- [`POST /loki/api/v1/rules/{namespace}`](#set-rule-group)
- [`DELETE /loki/api/v1/rules/{namespace}/{groupName}`](#delete-rule-group)
- [`DELETE /loki/api/v1/rules/{namespace}`](#delete-namespace)
- [`GET /api/prom/rules`](#list-rule-groups)
- [`GET /api/prom/rules/{namespace}`](#get-rule-groups-by-namespace)
- [`GET /api/prom/rules/{namespace}/{groupName}`](#get-rule-group)
- [`POST /api/prom/rules/{namespace}`](#set-rule-group)
- [`DELETE /api/prom/rules/{namespace}/{groupName}`](#delete-rule-group)
- [`DELETE /api/prom/rules/{namespace}`](#delete-namespace)
- [`GET /prometheus/api/v1/rules`](#list-rules)
- [`GET /prometheus/api/v1/alerts`](#list-alerts)

API endpoints starting with `/api/prom` are [Prometheus API-compatible](https://prometheus.io/docs/prometheus/latest/querying/api/) and the result formats can be used interchangeably.

### Log deletion endpoints

These endpoints are exposed by the `compactor`, `backend`, and `all` components:

- [`POST /loki/api/v1/delete`](#request-log-deletion)
- [`GET /loki/api/v1/delete`](#list-log-deletion-requests)
- [`DELETE /loki/api/v1/delete`](#request-cancellation-of-a-delete-request)

### Other endpoints

These HTTP endpoints are exposed by all individual components:

- [`GET /loki/api/v1/format_query`](#format-a-logql-query)

### Deprecated endpoints

{{< admonition type="note" >}}
The following endpoints are deprecated.While they still exist and work, they should not be used for new deployments.
Existing deployments should upgrade to use the supported endpoints.
{{< /admonition >}}

| Deprecated | Replacement |
| ---------- | ----------- |
| `POST /api/prom/push` | [`POST /loki/api/v1/push`](#ingest-logs) |
| `GET /api/prom/tail` | [`GET /loki/api/v1/tail`](#stream-logs) |
| `GET /api/prom/query` | [`GET /loki/api/v1/query`](#query-logs-at-a-single-point-in-time) |
| `GET /api/prom/label` | [`GET /loki/api/v1/labels`](#query-labels) |
| `GET /api/prom/label/<name>/values` | [`GET /loki/api/v1/label/<name>/values`](#query-label-values) |
| `GET /api/prom/series` | [`GET /loki/api/v1/series`](#query-streams) |

## Format

### Matrix, vector, and stream

Some Loki API endpoints return a result of a matrix, a vector, or a stream:

- **Matrix**: a table of values where each row represents a different label set
and the columns are each sample values for that row over the queried time.
Matrix types are only returned when running a query that computes some value.

- **Instant Vector**: denoted in the type as just `vector`, an Instant Vector
represents the latest value of a calculation for a given labelset. Instant
Vectors are only returned when doing a query against a single point in
time.

- **Stream**: a Stream is a set of all values (logs) for a given label set over the
queried time range. Streams are the only type that will result in log lines
being returned.

### Timestamps

The API accepts several formats for timestamps:

- All epoch values will be interpreted as a Unix timestamp in nanoseconds.
- A floating point number is a Unix timestamp with fractions of a second.
- A string in `RFC3339` and `RFC3339Nano` format, as supported by Go's [time](https://pkg.go.dev/time) package.

{{< admonition type="note" >}}
When using `/api/v1/push`, you must send the timestamp as a string and not a number, otherwise the endpoint will return a 400 error.
{{< /admonition >}}

### Statistics

Query endpoints such as `/loki/api/v1/query` and `/loki/api/v1/query_range` return a set of statistics about the query execution. Those statistics allow users to understand the amount of data processed and at which speed.

The example below show all possible statistics returned with their respective description.

```json
{
  "status": "success",
  "data": {
    "resultType": "streams",
    "result": [],
    "stats": {
      "ingester": {
        "compressedBytes": 0, // Total bytes of compressed chunks (blocks) processed by ingesters
        "decompressedBytes": 0, // Total bytes decompressed and processed by ingesters
        "decompressedLines": 0, // Total lines decompressed and processed by ingesters
        "headChunkBytes": 0, // Total bytes read from ingesters head chunks
        "headChunkLines": 0, // Total lines read from ingesters head chunks
        "totalBatches": 0, // Total batches sent by ingesters
        "totalChunksMatched": 0, // Total chunks matched by ingesters
        "totalDuplicates": 0, // Total of duplicates found by ingesters
        "totalLinesSent": 0, // Total lines sent by ingesters
        "totalReached": 0 // Amount of ingesters reached.
      },
      "store": {
        "compressedBytes": 0, // Total bytes of compressed chunks (blocks) processed by the store
        "decompressedBytes": 0, // Total bytes decompressed and processed by the store
        "decompressedLines": 0, // Total lines decompressed and processed by the store
        "chunksDownloadTime": 0, // Total time spent downloading chunks in seconds (float)
        "totalChunksRef": 0, // Total chunks found in the index for the current query
        "totalChunksDownloaded": 0, // Total of chunks downloaded
        "totalDuplicates": 0 // Total of duplicates removed from replication
      },
      "summary": {
        "bytesProcessedPerSecond": 0, // Total of bytes processed per second
        "execTime": 0, // Total execution time in seconds (float)
        "linesProcessedPerSecond": 0, // Total lines processed per second
        "queueTime": 0, // Total queue time in seconds (float)
        "totalBytesProcessed": 0, // Total amount of bytes processed overall for this request
        "totalLinesProcessed": 0 // Total amount of lines processed overall for this request
      }
    }
  }
}
```

### Processed-byte response header

Successful `/loki/api/v1/query`, `/loki/api/v1/query_range`, `/loki/api/v1/detected_fields`,
and `/loki/api/v1/detected_field/<name>/values` responses include
`X-Loki-Query-Bytes-Processed`: a non-negative decimal integer matching the final response's
`totalBytesProcessed` statistic, regardless of encoding or compression.

The total includes available statistics; missing contributions count as zero. A value of `0`
can mean no bytes were processed or no statistics were available. Failed queries and older
servers may omit the header; an absent header does not mean zero usage.

## Ingest logs

```bash
POST /loki/api/v1/push
```

`/loki/api/v1/push` is the endpoint used to send log entries to Loki. The default
behavior is for the POST body to be a [Snappy](https://github.com/google/snappy)-compressed [Protocol Buffer](https://github.com/protocolbuffers/protobuf) message:

- [Protocol Buffer definition](https://github.com/grafana/loki/blob/main/pkg/push/push.proto)
- [Go client library](https://github.com/grafana/loki/blob/main/clients/pkg/promtail/client/client.go)

These POST requests require the `Content-Type` HTTP header to be `application/x-protobuf`.

Alternatively, if the `Content-Type` header is set to `application/json`, a JSON post body can be sent in the following format:

```json
{
  "streams": [
    {
      "stream": {
        "label": "value"
      },
      "values": [
          [ "<unix epoch in nanoseconds>", "<log line>" ],
          [ "<unix epoch in nanoseconds>", "<log line>" ]
      ]
    }
  ]
}
```

{{< admonition type="note" >}}
Loki validates JSON push payloads strictly: a field name that doesn't belong to the schema shown above causes the request to fail with HTTP 400 and an error message that contains `found unknown field: <field>`. This also applies to the deprecated `/api/prom/push` endpoint, which uses a different JSON schema (`labels` and `entries` instead of `stream` and `values`). Each endpoint only accepts the fields in its own schema, so sending one endpoint's payload format to the other now returns an error instead of silently dropping data. This validation doesn't apply to the default Protocol Buffer encoding.
{{< /admonition >}}

You can set `Content-Encoding: gzip` request header and post gzipped JSON.

You can optionally attach [structured metadata](../../get-started/labels/structured-metadata/) to each log line by adding a JSON object to the end of the log line array.
The JSON object must be a valid JSON object with string keys and string values. The JSON object should not contain any nested object.
The JSON object must be set immediately after the log line. Here is an example of a log entry with some structured metadata attached:

```json
"values": [
    [ "<unix epoch in nanoseconds>", "<log line>", {"trace_id": "0242ac120002", "user_id": "superUser123"}]
]
```

In microservices mode, `/loki/api/v1/push` is exposed by the distributor.

If [`block_ingestion_until`](/docs/loki/<LOKI_VERSION>/configuration/#limits_config) is configured and push requests are blocked, the endpoint will return the status code configured in `block_ingestion_status_code` (`260` by default)
along with an error message. If the configured status code is `200`, no error message will be returned.

### Examples

The following cURL command pushes a stream with the label "foo=bar2" and a single log line "fizzbuzz" using JSON encoding:

```bash
curl -H "Content-Type: application/json" \
  -s -X POST "http://localhost:3100/loki/api/v1/push" \
  --data-raw '{"streams": [{ "stream": { "foo": "bar2" }, "values": [ [ "1570818238000000000", "fizzbuzz" ] ] }]}'
```

## Ingest logs using OTLP

```bash
POST /otlp/v1/logs
```

`/otlp/v1/logs` lets the OpenTelemetry Collector send logs to Loki using `otlphttp` protocol.

For information on how to configure Loki, refer to the [OTel Collector topic](https://grafana.com/docs/loki/<LOKI_VERSION>/send-data/otel/).

<!-- vale Google.Will = NO -->
{{< admonition type="note" >}}
When configuring the OpenTelemetry Collector, you must use `endpoint: http://<LOKI_ADDR>/otlp`, as the collector automatically completes the endpoint.  Entering the full endpoint will generate an error.
{{< /admonition >}}
<!-- vale Google.Will = YES -->

## Query logs at a single point in time

```bash
GET /loki/api/v1/query
```

`/loki/api/v1/query` allows for doing queries against a single point in time.
This type of query is often referred to as an instant query. Instant queries are only used for metric type LogQL queries
and will return a 400 (Bad Request) in case a log type query is provided.
The endpoint accepts the following query parameters in the URL:

- `query`: The [LogQL](../../query/) query to perform. Requests that do not use valid LogQL syntax will return errors.
- `limit`: The max number of entries to return. It defaults to `100`. Only applies to query types which produce a stream (log lines) response.
- `time`: The evaluation time for the query as a nanosecond Unix epoch or another [supported format](#timestamps). Defaults to now.
- `direction`: Determines the sort order of logs. Supported values are `forward` or `backward`. Defaults to `backward`.

In microservices mode, `/loki/api/v1/query` is exposed by the querier and the query frontend.

Response format:

```json
{
  "status": "success",
  "data": {
    "resultType": "vector" | "streams",
    "result": [<vector value>] | [<stream value>],
    "stats" : [<statistics>]
  }
}
```

where `<vector value>` is:

```json
{
  "metric": {
    <label key-value pairs>
  },
  "value": [
    <number: second unix epoch>,
    <string: value>
  ]
}
```

and `<stream value>` is:

```json
{
  "stream": {
    <label key-value pairs>
  },
  "values": [
    [
      <string: nanosecond unix epoch>,
      <string: log line>
    ],
    ...
  ]
}
```

The items in the `values` array are sorted by timestamp.
The most recent item is first when using `direction=backward`.
The oldest item is first when using `direction=forward`.

Parquet can be request as a response format by setting the `Accept` header to `application/vnd.apache.parquet`.

The schema is the following for streams:

| column_name |       column_type        |
|-------------|--------------------------|
| timestamp   | TIMESTAMP WITH TIME ZONE |
| labels      | MAP(VARCHAR, VARCHAR)    |
| line        |VARCHAR                   |

and for metrics:

| column_name |       column_type        |
|-------------|--------------------------|
| timestamp   | TIMESTAMP WITH TIME ZONE |
| labels      | MAP(VARCHAR, VARCHAR)    |
| value       | DOUBLE                   |

See [statistics](#statistics) for information about the statistics returned by Loki.

### Examples

This example cURL command

```bash
curl -G -s  "http://localhost:3100/loki/api/v1/query" \
  --data-urlencode 'query=sum(rate({job="varlogs"}[10m])) by (level)' | jq
```

gave this response:

```json
{
  "status": "success",
  "data": {
    "resultType": "vector",
    "result": [
      {
        "metric": {},
        "value": [
          1588889221,
          "1267.1266666666666"
        ]
      },
      {
        "metric": {
          "level": "warn"
        },
        "value": [
          1588889221,
          "37.77166666666667"
        ]
      },
      {
        "metric": {
          "level": "info"
        },
        "value": [
          1588889221,
          "37.69"
        ]
      }
    ],
    "stats": {
      ...
    }
  }
}
```

#### Multi-tenant queries

If your cluster has
[Grafana Loki Multi-Tenancy](../../operations/multi-tenancy/) enabled,
set the `X-Scope-OrgID` header to identify the tenant you want to query.
Here is the same example query for the single tenant called `Tenant1`:

```bash
curl -H 'X-Scope-OrgID:Tenant1' \
  -G -s "http://localhost:3100/loki/api/v1/query" \
  --data-urlencode 'query=sum(rate({job="varlogs"}[10m])) by (level)' | jq
```

To query against the three tenants `Tenant1`, `Tenant2`, and `Tenant3`,
specify the tenant names separated by the pipe (`|`) character:

```bash
curl -H 'X-Scope-OrgID:Tenant1|Tenant2|Tenant3' \
  -G -s "http://localhost:3100/loki/api/v1/query" \
  --data-urlencode 'query=sum(rate({job="varlogs"}[10m])) by (level)' | jq
```

The same example query for Grafana Enterprise Logs
uses Basic Authentication and specifies the tenant names as a `user`.
The tenant names are separated by the pipe (`|`) character.
The password in this example is an access policy token that has been
defined in the `API_TOKEN` environment variable:

```bash
curl -u "Tenant1|Tenant2|Tenant3:$API_TOKEN" \
  -G -s "http://localhost:3100/loki/api/v1/query" \
  --data-urlencode 'query=sum(rate({job="varlogs"}[10m])) by (level)' | jq
```

To query against your hosted log tenant in Grafana Cloud, use HTTP Basic Authentication with your Loki instance ID and an access policy token, as described in [Authentication](#authentication) above.

## Query logs within a range of time

```bash
GET /loki/api/v1/query_range
```

`/loki/api/v1/query_range` is used to do a query over a range of time.
This type of query is often referred to as a range query. Range queries are used for both log and metric type LogQL queries.
It accepts the following query parameters in the URL:

- `query`: The [LogQL](../../query/) query to perform.
- `limit`: The max number of entries to return. It defaults to `100`. Only applies to query types which produce a stream (log lines) response.
- `start`: The start time for the query as a nanosecond Unix epoch or another [supported format](#timestamps). Defaults to one hour ago. Loki returns results with timestamp greater or equal to this value.
- `end`: The end time for the query as a nanosecond Unix epoch or another [supported format](#timestamps). Defaults to now. Loki returns results with timestamp lower than this value.
- `since`: A `duration` used to calculate `start` relative to `end`. If `end` is in the future, `start` is calculated as this duration before now. Any value specified for `start` supersedes this parameter.
- `step`: Query resolution step width in `duration` format or float number of seconds. `duration` refers to Prometheus duration strings of the form `[0-9]+[smhdwy]`. For example, 5m refers to a duration of 5 minutes. Defaults to a dynamic value based on `start` and `end`. Only applies to query types which produce a matrix response.
- `interval`: Only return entries at (or greater than) the specified interval, can be a `duration` format or float number of seconds. Only applies to queries which produce a stream response. Not to be confused with `step`, see the explanation under [Step versus interval](#step-versus-interval).
- `direction`: Determines the sort order of logs. Supported values are `forward` or `backward`. Defaults to `backward.`

In microservices mode, `/loki/api/v1/query_range` is exposed by the querier and the query frontend.

### Step versus interval

Use the `step` parameter when making metric queries to Loki, or queries which return a matrix response. It is evaluated in exactly the same way Prometheus evaluates `step`. First the query will be evaluated at `start` and then evaluated again at `start + step` and again at `start + step + step` until `end` is reached. The result will be a matrix of the query result evaluated at each step.

Use the `interval` parameter when making log queries to Loki, or queries which return a stream response. It is evaluated by returning a log entry at `start`, then the next entry will be returned an entry with timestampe >= `start + interval`, and again at `start + interval + interval` and so on until `end` is reached. It does not fill missing entries.

Response format:

```json
{
  "status": "success",
  "data": {
    "resultType": "matrix" | "streams",
    "result": [<matrix value>] | [<stream value>]
    "stats" : [<statistics>]
  }
}
```

where `<matrix value>` is:

```json
{
  "metric": {
    <label key-value pairs>
  },
  "values": [
    [
      <number: second unix epoch>,
      <string: value>
    ],
    ...
  ]
}
```

The items in the `values` array are sorted by timestamp, and the oldest item is first.

And `<stream value>` is:

```json
{
  "stream": {
    <label key-value pairs>
  },
  "values": [
    [
      <string: nanosecond unix epoch>,
      <string: log line>
    ],
    ...
  ]
}
```

The items in the `values` array are sorted by timestamp.
The most recent item is first when using `direction=backward`.
The oldest item is first when using `direction=forward`.

Parquet can be request as a response format by setting the `Accept` header to `application/vnd.apache.parquet`.

The schema is the following for streams:

| column_name |       column_type        |
|-------------|--------------------------|
| timestamp   | TIMESTAMP WITH TIME ZONE |
| labels      | MAP(VARCHAR, VARCHAR)    |
| line        |VARCHAR                   |

and for metrics:

| column_name |       column_type        |
|-------------|--------------------------|
| timestamp   | TIMESTAMP WITH TIME ZONE |
| labels      | MAP(VARCHAR, VARCHAR)    |
| value       | DOUBLE                   |

See [statistics](#statistics) for information about the statistics returned by Loki.

### Examples

This example cURL command

```bash
curl -G -s  "http://localhost:3100/loki/api/v1/query_range" \
  --data-urlencode 'query=sum(rate({job="varlogs"}[10m])) by (level)' \
  --data-urlencode 'step=300' | jq
```

gave this response:

```json
{
  "status": "success",
  "data": {
    "resultType": "matrix",
    "result": [
      {
       "metric": {
          "level": "info"
        },
        "values": [
          [
            1588889221,
            "137.95"
          ],
          [
            1588889221,
            "467.115"
          ],
          [
            1588889221,
            "658.8516666666667"
          ]
        ]
      },
      {
        "metric": {
          "level": "warn"
        },
        "values": [
          [
            1588889221,
            "137.27833333333334"
          ],
          [
            1588889221,
            "467.69"
          ],
          [
            1588889221,
            "660.6933333333334"
          ]
        ]
      }
    ],
    "stats": {
      ...
    }
  }
}
```

This example cURL command

```bash
curl -G -s "http://localhost:3100/loki/api/v1/query_range" \
  --data-urlencode 'query={job="varlogs"}' | jq
```

gave this response:

```json
{
  "status": "success",
  "data": {
    "resultType": "streams",
    "result": [
      {
        "stream": {
          "filename": "/var/log/myproject.log",
          "job": "varlogs",
          "level": "info"
        },
        "values": [
          [
            "1569266497240578000",
            "foo"
          ],
          [
            "1569266492548155000",
            "bar"
          ]
        ]
      }
    ],
    "stats": {
      ...
    }
  }
}
```

## Query labels

```bash
GET /loki/api/v1/labels
```

`/loki/api/v1/labels` retrieves the list of known labels within a given time span.
Loki may use a larger time span than the one specified.
It accepts the following query parameters in the URL:

- `start`: The start time for the query as a nanosecond Unix epoch. Defaults to 6 hours ago.
- `end`: The end time for the query as a nanosecond Unix epoch. Defaults to now.
- `since`: A `duration` used to calculate `start` relative to `end`. If `end` is in the future, `start` is calculated as this duration before now. Any value specified for `start` supersedes this parameter.
- `query`: Log stream selector that selects the streams to match and return label names. Example: `{app="myapp", environment="dev"}`

In microservices mode, `/loki/api/v1/labels` is exposed by the querier.

Response format:

```bash
{
  "status": "success",
  "data": [
    <label string>,
    ...
  ]
}
```

### Examples

This example cURL command

```bash
curl -G -s  "http://localhost:3100/loki/api/v1/labels" | jq
```

gave this response:

```json
{
  "status": "success",
  "data": [
    "foo",
    "bar",
    "baz"
  ]
}
```

## Query label values

```bash
GET /loki/api/v1/label/<name>/values
```

`/loki/api/v1/label/<name>/values` retrieves the list of known values for a given
label within a given time span. Loki may use a larger time span than the one specified.
It accepts the following query parameters in the URL:

- `start`: The start time for the query as a nanosecond Unix epoch. Defaults to 6 hours ago.
- `end`: The end time for the query as a nanosecond Unix epoch. Defaults to now.
- `since`: A `duration` used to calculate `start` relative to `end`. If `end` is in the future, `start` is calculated as this duration before now. Any value specified for `start` supersedes this parameter.
- `query`: Log stream selector that selects the streams to match and return label values for `<name>`. Example: `{app="myapp", environment="dev"}`

In microservices mode, `/loki/api/v1/label/<name>/values` is exposed by the querier.

Response format:

```json
{
  "status": "success",
  "data": [
    <label value>,
    ...
  ]
}
```

### Examples

This example cURL command

```bash
curl -G -s  "http://localhost:3100/loki/api/v1/label/foo/values" | jq
```

gave this response:

```json
{
  "status": "success",
  "data": [
    "cat",
    "dog",
    "axolotl"
  ]
}
```

## Query streams

The Series API is available under the following:

- `GET /loki/api/v1/series`
- `POST /loki/api/v1/series`

This endpoint returns the list of streams (unique set of labels) that match a certain given selector.

URL query parameters:

- `match[]=<selector>`: Repeated log stream selector argument that selects the streams to return. At least one `match[]` argument must be provided.
- `start=<nanosecond Unix epoch>`: Start timestamp.
- `end=<nanosecond Unix epoch>`: End timestamp.
- `since`: A `duration` used to calculate `start` relative to `end`. If `end` is in the future, `start` is calculated as this duration before now. Any value specified for `start` supersedes this parameter.

You can URL-encode these parameters directly in the request body by using the POST method and `Content-Type: application/x-www-form-urlencoded` header. This is useful when specifying a large or dynamic number of stream selectors that may breach server-side URL character limits.

In microservices mode, these endpoints are exposed by the querier.

### Examples

This example cURL command

```bash
curl -s "http://localhost:3100/loki/api/v1/series" \
  --data-urlencode 'match[]={container_name=~"prometheus.*", component="server"}' \
  --data-urlencode 'match[]={app="loki"}' | jq '.'
```

gave this response:

```json
{
  "status": "success",
  "data": [
    {
      "container_name": "loki",
      "app": "loki",
      "stream": "stderr",
      "filename": "/var/log/pods/default_loki-stack-0_50835643-1df0-11ea-ba79-025000000001/loki/0.log",
      "name": "loki",
      "job": "default/loki",
      "controller_revision_hash": "loki-stack-757479754d",
      "statefulset_kubernetes_io_pod_name": "loki-stack-0",
      "release": "loki-stack",
      "namespace": "default",
      "instance": "loki-stack-0"
    },
    {
      "chart": "prometheus-9.3.3",
      "container_name": "prometheus-server-configmap-reload",
      "filename": "/var/log/pods/default_loki-stack-prometheus-server-696cc9ddff-87lmq_507b1db4-1df0-11ea-ba79-025000000001/prometheus-server-configmap-reload/0.log",
      "instance": "loki-stack-prometheus-server-696cc9ddff-87lmq",
      "pod_template_hash": "696cc9ddff",
      "app": "prometheus",
      "component": "server",
      "heritage": "Tiller",
      "job": "default/prometheus",
      "namespace": "default",
      "release": "loki-stack",
      "stream": "stderr"
    },
    {
      "app": "prometheus",
      "component": "server",
      "filename": "/var/log/pods/default_loki-stack-prometheus-server-696cc9ddff-87lmq_507b1db4-1df0-11ea-ba79-025000000001/prometheus-server/0.log",
      "release": "loki-stack",
      "namespace": "default",
      "pod_template_hash": "696cc9ddff",
      "stream": "stderr",
      "chart": "prometheus-9.3.3",
      "container_name": "prometheus-server",
      "heritage": "Tiller",
      "instance": "loki-stack-prometheus-server-696cc9ddff-87lmq",
      "job": "default/prometheus"
    }
  ]
}
```

## Query log statistics

```bash
GET /loki/api/v1/index/stats
```

The `/loki/api/v1/index/stats` endpoint can be used to query the index for the number of `streams`, `chunks`, `entries`, and `bytes` that a query resolves to.

URL query parameters:

- `query`: The [LogQL](../../query/) matchers to check (that is, `{job="foo", env!="dev"}`)
- `start=<nanosecond Unix epoch>`: Start timestamp.
- `end=<nanosecond Unix epoch>`: End timestamp.

You can URL-encode these parameters directly in the request body by using the POST method and `Content-Type: application/x-www-form-urlencoded` header. This is useful when specifying a large or dynamic number of stream selectors that may breach server-side URL character limits.

Response:

```json
{
  "streams": 100,
  "chunks": 1000,
  "entries": 5000,
  "bytes": 100000
}
```

It is an approximation with the following caveats:

- It does not include data from the ingesters.
- It is a probabilistic technique.
- Streams/chunks which span multiple period configurations may be counted twice.

These make it generally more helpful for larger queries.
It can be used for better understanding the throughput requirements and data topology for a list of matchers over a period of time.

## Query log volume

```bash
GET /loki/api/v1/index/volume
GET /loki/api/v1/index/volume_range
```

{{< admonition type="note" >}}
This feature is controlled by `volume_enabled`, which defaults to `true`. Set it to `false` to disable it.
{{< /admonition >}}

The `/loki/api/v1/index/volume` and `/loki/api/v1/index/volume_range` endpoints can be used to query the index for volume information about label and label-value combinations. This is helpful in exploring the logs Loki has ingested to find high or low volume streams. The `volume` endpoint returns results for a single point in time, the time the query was processed. Each datapoint represents an aggregation of the matching label or series over the requested time period, returned in a Prometheus style vector response. The `volume_range` endoint returns a series of datapoints over a range of time, in Prometheus style matrix response, for each matching set of labels or series. The number of timestamps returned when querying `volume_range` will be determined by the provided `step` parameter and the requested time range.

The `query` should be a valid LogQL stream selector, for example `{job="foo", env=~".+"}`. By default, these endpoints will aggregate into series consisting of all matches for labels included in the query. For example, assuming you have the streams `{job="foo", env="prod", team="alpha"}`, `{job="bar", env="prod", team="beta"}`, `{job="foo", env="dev", team="alpha"}`, and `{job="bar", env="dev", team="beta"}` in your system. The query `{job="foo", env=~".+"}` would return the two metric series `{job="foo", env="dev"}` and `{job="foo", env="prod"}`, each with datapoints representing the accumulate values of chunks for the streams matching that selector, which in this case would be the streams `{job="foo", env="dev", team="alpha"}` and `{job="foo", env="prod", team="alpha"}`, respectively.

There are two parameters which can affect the aggregation strategy. First, a comma-separated list of `targetLabels` can be provided, allowing volumes to be aggregated by the speficied `targetLabels` only. This is useful for negations. For example, if you said `{team="alpha", env!="dev"}`, the default behavior would include `env` in the aggregation set. However, maybe you're looking for all non-dev jobs for team alpha, and you don't care which env those are in (other than caring that they're not dev jobs). To achieve this, you could specify `targetLabels=team,job`, resulting in a single metric series (in this case) of `{team="alpha", job="foo}`.

The other way to change aggregations is with the `aggregateBy` parameter. The default value for this is `series`, which aggregates into combinations of matching key-value pairs. Alternately this can be specified as `labels`, which will aggregate into labels only. In this case, the response will have a metric series with a label name matching each label, and a label value of `""`. This is useful for exploring logs at a high level. For example, if you wanted to know what percentage of your logs had a `team` label, you could query your logs with `aggregateBy=labels` and a query with either an exact or regex match on `team`, or by including `team` in the list of `targetLabels`.

URL query parameters:

- `query`: The [LogQL](../../query/) matchers to check (that is, `{job="foo", env=~".+"}`). This parameter is required.
- `start=<nanosecond Unix epoch>`: Start timestamp. This parameter is required.
- `end=<nanosecond Unix epoch>`: End timestamp. This parameter is required.
- `limit`: How many metric series to return. The parameter is optional, the default is `100`.
- `step`: Query resolution step width in `duration` format or float number of seconds. `duration` refers to Prometheus duration strings of the form `[0-9]+[smhdwy]`. For example, 5m refers to a duration of 5 minutes. Defaults to a dynamic value based on `start` and `end`. This parameter is optional, and only applicable when querying the `volume_range` endpoint, which will always return a Prometheus style matrix response. The default step configured for range queries will be used when not provided.
- `targetLabels`: A comma separated list of labels to aggregate into. This parameter is optional. When not provided, volumes will be aggregated into the matching labels or label-value pairs.
- `aggregateBy`: Whether to aggregate into labels or label-value pairs. This parameter is optional, the default is label-value pairs.

You can URL-encode these parameters directly in the request body by using the POST method and `Content-Type: application/x-www-form-urlencoded` header. This is useful when specifying a large or dynamic number of stream selectors that may breach server-side URL character limits.

## Patterns detection

```bash
GET /loki/api/v1/patterns
```

{{< admonition type="note" >}}
You must configure

```yaml
pattern_ingester:
  enabled: true
```

to enable this feature.
{{< /admonition >}}

The `/loki/api/v1/patterns` endpoint can be used to query loki for patterns detected in the logs. This helps understand the structure of the logs Loki has ingested.

The `query` should be a valid LogQL stream selector, for example `{job="foo", env=~".+"}`. The result is aggregated by the `pattern` from all matching streams.

For each pattern detected, the response includes the pattern itself and the number of samples for each pattern at each timestamp.

For example, if you have the following logs:

```log
ts=2024-03-30T23:03:40 caller=grpc_logging.go:66 level=info method=/cortex.Ingester/Push duration=200ms msg=gRPC
ts=2024-03-30T23:03:41 caller=grpc_logging.go:66 level=info method=/cortex.Ingester/Push duration=500ms msg=gRPC
```

The pattern detected might be:

```log
ts=<_> caller=grpc_logging.go:66 level=info method=/cortex.Ingester/Push duration=<_> msg=gRPC
```

URL query parameters:

- `query`: The [LogQL](../../query/) matchers to check (that is, `{job="foo", env=~".+"}`). This parameter is required.
- `start=<nanosecond Unix epoch>`: Start timestamp. This parameter is required.
- `end=<nanosecond Unix epoch>`: End timestamp. This parameter is required.
- `step=<duration string or float number of seconds>`: Step between samples for occurrences of this pattern. This parameter is optional.

### Examples

This example cURL command

```bash
curl -s "http://localhost:3100/loki/api/v1/patterns" \
  --data-urlencode 'query={app="loki"}' | jq
```

gave this response:

```json
{
  "status": "success",
  "data": [
    {
      "pattern": "<_> caller=grpc_logging.go:66 <_> level=error method=/cortex.Ingester/Push <_> msg=gRPC err=\"connection refused to object store\"",
      "samples": [
        [
          1711839260,
          1
        ],
        [
          1711839270,
          2
        ],
        [
          1711839280,
          1
        ]
      ]
    },
    {
      "pattern": "<_> caller=grpc_logging.go:66 <_> level=info method=/cortex.Ingester/Push <_> msg=gRPC",
      "samples": [
        [
          1711839260,
          105
        ],
        [
          1711839270,
          222
        ],
        [
          1711839280,
          196
        ]
      ]
    }
  ]
}
```

The result is a list of patterns detected in the logs, with the number of samples for each pattern at each timestamp.
The pattern format is the same as the [LogQL](../../query/) pattern filter and parser and can be used in queries for filtering matching logs.
Each sample is a tuple of timestamp (second) and count.

## Detected fields

```bash
GET /loki/api/v1/detected_fields
POST /loki/api/v1/detected_fields
```

The `/loki/api/v1/detected_fields` endpoint returns fields that Loki has detected in log lines matching the given stream selector, along with the inferred type, estimated cardinality, and the parser that was used to extract the field.
This endpoint is useful for discovering the structure of your logs without running a full log query.

URL query parameters:

- `query`: The [LogQL](../../query/) stream selector to match. Example: `{app="myapp", environment="dev"}`. This parameter is required.
- `start=<nanosecond Unix epoch>`: Start timestamp. This parameter is optional. If omitted, it defaults to `end - since`.
- `end=<nanosecond Unix epoch>`: End timestamp. This parameter is optional. If omitted, it defaults to the current server time.
- `since=<duration>`: Relative time range (for example, `1h`, `5m`). This parameter is optional and is used when `start` is omitted to compute `start = end - since`. If both `start` and `since` are omitted, `since` defaults to `1h`.
- `step=<duration string or float number of seconds>`: Step between sample windows. This parameter is optional.
- `line_limit=<integer>`: Maximum number of log lines to scan per shard. Defaults to 100. This parameter is optional.
- `limit=<integer>`: Maximum number of fields to return. Defaults to 1000. The query parameter `field_limit` is also accepted as an alias for backwards compatibility. This parameter is optional.

You can URL-encode these parameters directly in the request body by using the POST method and `Content-Type: application/x-www-form-urlencoded` header.

Response format:

```bash
{
  "fields": [
    {
      "label": <string>,
      "type": <string|int|float|boolean|duration|bytes>,
      "cardinality": <integer>,
      "parsers": [<string>],
      "jsonPath": <string, optional>,
      "sketch": <object, optional>
    }
  ],
  "limit": <integer>,
  "stats": <statistics object>
}
```

The `stats` object uses the same [statistics structure](#statistics) as regular queries and
reports the underlying log scan. It is included even when no fields or values are returned.
The `limit` parameter limits returned metadata, not the reported scan statistics.

### Examples

This example cURL command

```bash
curl -H 'X-Scope-OrgID: <TENANT_ID>' -G -s "http://localhost:3100/loki/api/v1/detected_fields" \
  --data-urlencode 'query={app="myapp"}' \
  --data-urlencode 'start=1609459200000000000' \
  --data-urlencode 'end=1609462800000000000' | jq
```

gave this response (other statistics fields omitted for brevity):

```json
{
  "fields": [
    {
      "label": "level",
      "type": "string",
      "cardinality": 3,
      "parsers": ["logfmt"]
    },
    {
      "label": "duration",
      "type": "duration",
      "cardinality": 152,
      "parsers": ["logfmt"]
    },
    {
      "label": "status",
      "type": "int",
      "cardinality": 5,
      "parsers": ["logfmt"]
    }
  ],
  "limit": 1000,
  "stats": {
    "summary": {
      "totalBytesProcessed": 1024
    }
  }
}
```

## Detected field values

```bash
GET /loki/api/v1/detected_field/{name}/values
POST /loki/api/v1/detected_field/{name}/values
```

The `/loki/api/v1/detected_field/{name}/values` endpoint returns the values observed for a specific detected field matching the given stream selector.
`{name}` is the name of the field to retrieve values for.

URL query parameters:

- `query`: The [LogQL](../../query/) stream selector to match. Example: `{app="myapp", environment="dev"}`. This parameter is required.
- `start=<nanosecond Unix epoch>`: Start timestamp. This parameter is optional. If omitted, it defaults to `end - since`.
- `end=<nanosecond Unix epoch>`: End timestamp. This parameter is optional. If omitted, it defaults to the current server time.
- `since=<duration>`: Relative time range (for example, `1h`, `5m`). This parameter is optional and is used when `start` is omitted to compute `start = end - since`. If both `start` and `since` are omitted, `since` defaults to `1h`.
- `step=<duration string or float number of seconds>`: Step between sample windows. This parameter is optional.
- `line_limit=<integer>`: Maximum number of log lines to scan per shard. Defaults to 100. This parameter is optional.
- `limit=<integer>`: Maximum number of values to return. Defaults to 1000. The query parameter `field_limit` is also accepted as an alias. This parameter is optional.

You can URL-encode these parameters directly in the request body by using the POST method and `Content-Type: application/x-www-form-urlencoded` header.

Response format:

```bash
{
  "values": [
    <string>,
    ...
  ],
  "limit": <integer>,
  "stats": <statistics object>
}
```

The `stats` object uses the same [statistics structure](#statistics) as regular queries and
reports the underlying log scan. It is included even when no fields or values are returned.
The `limit` parameter limits returned metadata, not the reported scan statistics.

### Examples

This example cURL command

```bash
curl -H 'X-Scope-OrgID: <TENANT_ID>' -G -s "http://localhost:3100/loki/api/v1/detected_field/level/values" \
  --data-urlencode 'query={app="myapp"}' \
  --data-urlencode 'start=1609459200000000000' \
  --data-urlencode 'end=1609462800000000000' | jq
```

gave this response (other statistics fields omitted for brevity):

```json
{
  "values": [
    "debug",
    "info",
    "warn",
    "error"
  ],
  "limit": 1000,
  "stats": {
    "summary": {
      "totalBytesProcessed": 1024
    }
  }
}
```

## Stream logs

```bash
GET /loki/api/v1/tail
```

`/loki/api/v1/tail` is a WebSocket endpoint that streams log messages based on a query to the client.
It accepts the following query parameters in the URL:

- `query`: The [LogQL](../../query/) query to perform.
- `delay_for`: The number of seconds to delay retrieving logs to let slow
  loggers catch up. Defaults to 0 and cannot be larger than 5.
- `limit`: The max number of entries to return. It defaults to `100`.
- `start`: The start time for the query as a nanosecond Unix epoch. Defaults to one hour ago.

In microservices mode, `/loki/api/v1/tail` is exposed by the querier.

{{< admonition type="note" >}}
The `tail` endpoint is designed for near real-time observation of a log stream. The initial lookback from `start` to the current time is best-effort and isn't guaranteed to return every matching log line, so this endpoint isn't suitable for retrieving complete log history. To reliably retrieve a large or complete range of logs, make repeated calls to [`/loki/api/v1/query_range`](#query-logs-within-a-range-of-time).
{{< /admonition >}}

Response format (streamed):

```json
{
  "streams": [
    {
      "stream": {
        <label key-value pairs>
      },
      "values": [
        [
          <string: nanosecond unix epoch>,
          <string: log line>
        ]
      ]
    }
  ],
  "dropped_entries": [
    {
      "labels": {
        <label key-value pairs>
      },
      "timestamp": "<nanosecond unix epoch>"
    }
  ]
}
```

## Readiness probe

```bash
GET /ready
```

`/ready` returns HTTP 200 when the Loki instance is ready to accept traffic. If
running Loki on Kubernetes, `/ready` can be used as a readiness probe.

In microservices mode, the `/ready` endpoint is exposed by all components.

## Change log level

```bash
GET /log_level
POST /log_level
```

`/log_level` a `GET` returns the current log level and a `POST` lets you change the log level of a Loki process **at runtime**.
This can be useful for accessing debugging information during an incident. Caution should be used when running at the `debug` log level, as this produces a large volume of data.

The endpoint accepts the following query parameters in the URL:

- `log_level`: A valid log level that can be passed as a URL param (`?log_level=<level>`) or as a form value in case of `POST`. Valid levels: [debug, info, warn, error]

In microservices mode, the `/log_level` endpoint is exposed by all components.

## Prometheus metrics

```bash
GET /metrics
```

`/metrics` returns exposed Prometheus metrics. See
[Observing Loki](../../operations/meta-monitoring/)
for a list of exported metrics.

In microservices mode, the `/metrics` endpoint is exposed by all components.

## Show current configuration

```bash
GET /config
```

`/config` exposes the current configuration. The optional `mode` query parameter can be used to
modify the output. If it has the value `diff` only the differences between the default configuration
and the current are returned. A value of `defaults` returns the default configuration.

In microservices mode, the `/config` endpoint is exposed by all components.

## Show exposed configuration

```bash
GET /loki/api/v1/exposed-config
```

`/loki/api/v1/exposed-config` returns a fixed, user-configured subset of `/config`, set with
`-config.exposed-fields`. It's meant for cases where `/config` itself isn't safe to expose directly,
such as through a datasource proxy. Empty by default, meaning the endpoint returns `{}`.

In microservices mode, the `/loki/api/v1/exposed-config` endpoint is exposed by all components.

## List running services

```bash
GET /services
```

`/services` returns a list of all running services and their current states.

Services can have the following states:

- **New**: Service is new, not running yet (initial state)
- **Starting**: Service is starting; if starting succeeds, service enters **Running** state
- **Running**: Service is fully running now; when service stops running, it enters **Stopping** state
- **Stopping**: Service is shutting down
- **Terminated**: Service has stopped successfully (terminal state)
- **Failed**: Service has failed in **Starting**, **Running** or **Stopping** state (terminal state)

## Show build information

```bash
GET /loki/api/v1/status/buildinfo
```

`/loki/api/v1/status/buildinfo` exposes the build information in a JSON object. The fields are `version`, `revision`, `branch`, `buildDate`, `buildUser`, and `goVersion`.

## Flush in-memory chunks to backing store

```bash
POST /flush
```

`/flush` triggers a flush of all in-memory chunks held by the ingesters to the
backing store. By default the flush is queued and the request returns without 
waiting for it to complete.

**URL query parameters:**

- `flush_index` (experimental):
  If set to `true`, the request waits for the chunks to be flushed, and then also forces the ingester's in-memory index to be built and shipped to the backing store.
  The flushed chunks are then immediately referenceable, rather than waiting for the next periodic index rotation.
  Waiting for the flush can take a long time, so set a generous client timeout.
  An ingester runs only one `flush_index` request at a time, across `/flush` and `/flush/tenant`.
  If another one is in progress, the request returns `429 Too Many Requests` without flushing anything, and you can retry it later.
  A bare `flush_index` is treated as `true`.
  If omitted, the index is not flushed and the request returns as soon as the flushes are queued.
  Requests without `flush_index` are never rejected because of an index flush in progress.

In microservices mode, the `/flush` endpoint is exposed by the ingester.

## Flush in-memory chunks for a tenant (experimental)

```bash
POST /flush/tenant
```

This endpoint is experimental and may change or be removed.

`/flush/tenant` triggers a flush of the in-memory chunks held by an ingester for
a single tenant to the backing store. By default the flush is queued and the
request returns without waiting for it to complete.

**URL query parameters:**

- `tenant=<tenant ID>`:
  Required. The tenant to flush.
- `matchers=<selector>`:
  Optional log stream selector that selects the streams to flush, for example `{app="foo"}`.
  If omitted, all in-memory streams for the tenant are flushed.
- `flush_index`:
  If set to `true`, the request waits for the chunks to be flushed, and then also forces the ingester's in-memory index to be built and shipped to the backing store.
  The flushed chunks are then immediately referenceable, rather than waiting for the next periodic index rotation.
  The index is shared by all tenants on the ingester, so the index flush is not tenant-scoped.
  Waiting for the flush can take a long time, so set a generous client timeout.
  An ingester runs only one `flush_index` request at a time, across all tenants and across `/flush` and `/flush/tenant`.
  If another one is in progress, the request returns `429 Too Many Requests` without flushing anything, and you can retry it later.
  A bare `flush_index` is treated as `true`.
  If omitted, the index is not flushed and the request returns as soon as the flushes are queued.
  Requests without `flush_index` are never rejected because of an index flush in progress.

For example, `POST /flush/tenant?tenant=foo&matchers={app="bar"}&flush_index`.

In microservices mode, the `/flush/tenant` endpoint is exposed by the ingester.

## Sync indexes from object storage

```bash
PUT /sync-indexes
GET /sync-indexes
```

`/sync-indexes` lets a caller force an index-gateway to refresh its object-storage
listing cache and download any newly shipped index files on demand, instead of
waiting for the periodic resync (which re-lists through a cache that can take up
to a minute to expire). It is the second step of making a freshly flushed index
queryable across read nodes:

1. `POST /flush/tenant?tenant=<tenant>&flush_index` on the ingester ships the new index to object storage.
1. `PUT /sync-indexes` on the index-gateway downloads it.
1. `POST /loki/api/v1/cache/generation_numbers/increase` on the compactor
   invalidates any cached query results so the new data is reflected.

This endpoint is not tenant-scoped; it refreshes the listing for all tables the
index-gateway already tracks.

A `PUT` request triggers a sync asynchronously and returns immediately:

- `202 Accepted` if a new sync was started.
- `409 Conflict` if a sync (manual or the periodic one) is already in progress, in
  which case the request is a no-op.

Because a `PUT` is a no-op while another sync is running, and a sync can be
long-running on a gateway that tracks many tables, callers should not assume a
single `PUT` ran the sync: re-issue the `PUT` until it is accepted, then poll
`GET` until the sync completes.

A `GET` request reports the sync status of each synced index as a JSON array, with
one entry per index (the index-gateway runs an independent sync per schema period):

```json
[
  {
    "name": "s3_2024-04-01",
    "in_progress": true,
    "last_trigger": "manual",
    "current_duration": "12s",
    "last_duration": "1m30s"
  },
  {
    "name": "s3_2024-10-01",
    "in_progress": false,
    "last_trigger": "periodic",
    "last_duration": "5s"
  },
  {
    "name": "s3_2025-04-01",
    "in_progress": false,
    "last_trigger": "never_triggered",
    "last_duration": "0s"
  }
]
```

Each entry reports (durations are Go duration strings, e.g. `"1m30s"`):

- `name`: identifies the index (the schema period's store name).
- `in_progress`: whether a sync (manual or periodic) is currently running for this index.
- `last_trigger`: what triggered the current or most recent sync — `manual` or `periodic`, or `never_triggered` if no sync has run yet.
- `current_duration`: how long the in-progress sync has been running (present only when `in_progress` is `true`).
- `last_duration`: how long the previous completed sync took (`"0s"` if none has completed yet).

If the configured index store does not support on-demand syncing, both `PUT` and `GET` return `503 Service Unavailable`.

In microservices mode, the `/sync-indexes` endpoint is exposed by the index-gateway.

## Prepare ingester shutdown

```bash
GET, POST, DELETE /ingester/prepare_shutdown
```

This endpoint is used to tell the ingester to release all resources on receiving the next `SIGTERM` or `SIGINT` signal.

A `POST` request to the `/ingester/prepare_shutdown` endpoint configures the ingester for a full shutdown and returns immediately.
Only when the ingester process is stopped with `SIGINT` or `SIGTERM`, it will unregister from the ring, and in-memory data will be flushed to long-term storage.
This endpoint supersedes any YAML configurations and isn't necessary if the ingester is already configured to unregister from the ring or to flush on shutdown.

A `GET` request to the `/ingester/prepare_shutdown` endpoint returns the status of this configuration, either `set` or `unset`.

A `DELETE` request to the `/ingester/prepare_shutdown` endpoint reverts the configuration of the ingester to its previous state
(with respect to unregistering on shutdown and flushing of in-memory data to long-term storage).

This API endpoint is usually used by Kubernetes-specific scale down automations such as the
[rollout-operator](https://github.com/grafana/rollout-operator).

## Flush in-memory chunks and shut down

```bash
GET, POST /ingester/shutdown
```

`/ingester/shutdown` triggers a shutdown of the ingester and notably will _always_ flush any in memory chunks it holds.
This is helpful for scaling down WAL-enabled ingesters where we want to ensure old WAL directories are not orphaned,
but instead flushed to our chunk backend.

It accepts three URL query parameters `flush`, `delete_ring_tokens`, and `terminate`.

**URL query parameters:**

- `flush=<bool>`:
  Flag to control whether to flush any in-memory chunks the ingester holds. Defaults to `true`.
- `delete_ring_tokens=<bool>`:
  Flag to control whether to delete the file that contains the ingester ring tokens of the instance if the `-ingester.token-file-path` is specified. Defaults to `false`.
- `terminate=<bool>`:
  Flag to control whether to terminate the Loki process after service shutdown. Defaults to `true`.

This handler, in contrast to the deprecated `/ingester/flush_shutdown` handler, terminates the Loki process by default.
This behaviour can be changed by setting the `terminate` query parameter to `false`.

In microservices mode, the `/ingester/shutdown` endpoint is exposed by the ingester.

## Distributor ring status

```bash
GET /distributor/ring
```

Displays a web page with the distributor hash ring status, including the state, health, and last heartbeat time of each distributor.

## Index gateway ring status

```bash
GET /indexgateway/ring
```

Displays a web page with the index gateway hash ring status, including the state, health, and last heartbeat time of each index gateway.

## Ruler

The ruler API endpoints require to configure a backend object storage to store the recording rules and alerts. The ruler API uses the concept of a "namespace" when creating rule groups. This is a stand-in for the name of the rule file in Prometheus. Rule groups must be named uniquely within a namespace.

{{< admonition type="note" >}}
This feature is controlled by `enable_api`, which defaults to `true`. Set it to `false` to disable it.
{{< /admonition >}}

### Ruler ring status

```bash
GET /ruler/ring
```

Displays a web page with the ruler hash ring status, including the state, health, and last heartbeat time of each ruler.

### List rule groups

```bash
GET /loki/api/v1/rules
```

List all rules configured for the authenticated tenant. This endpoint returns a YAML dictionary with all the rule groups for each namespace and `200` status code on success.

#### Example response

```yaml
---
<namespace1>:
- name: <string>
  interval: <duration;optional>
  rules:
  - alert: <string>
    expr: <string>
    for: <duration>
    annotations:
      <annotation_name>: <string>
    labels:
      <label_name>: <string>
- name: <string>
  interval: <duration;optional>
  rules:
  - alert: <string>
    expr: <string>
    for: <duration>
    annotations:
      <annotation_name>: <string>
    labels:
      <label_name>: <string>
<namespace2>:
- name: <string>
  interval: <duration;optional>
  rules:
  - alert: <string>
    expr: <string>
    for: <duration>
    annotations:
      <annotation_name>: <string>
    labels:
      <label_name>: <string>
```

### Get rule groups by namespace

```bash
GET /loki/api/v1/rules/{namespace}
```

Returns the rule groups defined for a given namespace.

#### Example response

```yaml
name: <string>
interval: <duration;optional>
rules:
  - alert: <string>
    expr: <string>
    for: <duration>
    annotations:
      <annotation_name>: <string>
    labels:
      <label_name>: <string>
```

### Get rule group

```bash
GET /loki/api/v1/rules/{namespace}/{groupName}
```

Returns the rule group matching the request namespace and group name.

### Set rule group

```bash
POST /loki/api/v1/rules/{namespace}
```

Creates or updates a rule group. This endpoint expects a request with `Content-Type: application/yaml` header and the rules **YAML** definition in the request body, and returns `202` on success.

#### Example request

Request headers:

- `Content-Type: application/yaml`

Request body:

```yaml
name: <string>
interval: <duration;optional>
rules:
  - alert: <string>
    expr: <string>
    for: <duration>
    annotations:
      <annotation_name>: <string>
    labels:
      <label_name>: <string>
```

### Delete rule group

```bash
DELETE /loki/api/v1/rules/{namespace}/{groupName}

```

Deletes a rule group by namespace and group name. This endpoints returns `202` on success.

### Delete namespace

```bash
DELETE /loki/api/v1/rules/{namespace}
```

Deletes all the rule groups in a namespace (including the namespace itself). This endpoint returns `202` on success.

### List rules

```bash
GET /prometheus/api/v1/rules?type={alert|record}&file={}&rule_group={}&rule_name={}
```

Prometheus-compatible rules endpoint to list alerting and recording rules that are currently loaded.

The `type` parameter is optional. If set, only the specified type of rule is returned.

The `file`, `rule_group` and `rule_name` parameters are optional, and can accept multiple values. If set, the response content is filtered accordingly.

For more information, refer to the [Prometheus rules](https://prometheus.io/docs/prometheus/latest/querying/api/#rules) documentation.

### List alerts

```bash
GET /prometheus/api/v1/alerts
```

Prometheus-compatible rules endpoint to list all active alerts.

For more information, refer to the Prometheus [alerts](https://prometheus.io/docs/prometheus/latest/querying/api/#alerts) documentation.

## Compactor

### Compactor ring status

```bash
GET /compactor/ring
```

Displays a web page with the compactor hash ring status, including the state, health, and last heartbeat time of each compactor.

### Request log deletion

```bash
POST /loki/api/v1/delete
PUT /loki/api/v1/delete
```

Create a new delete request for the authenticated tenant.
The [log entry deletion](https://grafana.com/docs/loki/<LOKI_VERSION>/operations/storage/logs-deletion/) documentation has configuration details.

Log entry deletion is supported when the TSDB index is configured for the index store. It is also supported on the deprecated BoltDB Shipper index, but BoltDB Shipper is being removed in Loki 4.0, so new deployments should use TSDB.

Query parameters:

- `query=<series_selector>`: query argument that identifies the streams from which to delete with optional line filters.
- `start=<rfc3339 | unix_seconds_timestamp>`: A timestamp that identifies the start of the time window within which entries will be deleted. This parameter is required.
- `end=<rfc3339 | unix_seconds_timestamp>`: A timestamp that identifies the end of the time window within which entries will be deleted. If not specified, defaults to the current time.
- `max_interval=<duration>`: The maximum time period the delete request can span. If the request is larger than this value, it is split into several requests of <= `max_interval`. Valid time units are `s`, `m`, and `h`.

A 204 response indicates success.

The query parameter can also include filter operations. For example `query={foo="bar"} |= "other"` will filter out lines that contain the string "other" for the streams matching the stream selector `{foo="bar"}`.

#### Examples

URL encode the `query` parameter. This sample form of a cURL command URL encodes `query={foo="bar"}`:

```bash
curl -g -X POST \
  'http://127.0.0.1:3100/loki/api/v1/delete?query={foo="bar"}&start=1591616227&end=1591619692' \
  -H 'X-Scope-OrgID: 1'
```

The same example deletion request for Grafana Enterprise Logs uses Basic Authentication and specifies the tenant name as a user; `Tenant1` is the tenant name in this example. The password in this example is an access policy token that has been defined in the API_TOKEN environment variable. The token must be for an access policy with `logs:delete` scope for the tenant specified in the user field:

```bash
curl -u "Tenant1:$API_TOKEN" \
  -g -X POST \
  'http://127.0.0.1:3100/loki/api/v1/delete?query={foo="bar"}&start=1591616227&end=1591619692'
```

### List log deletion requests

```bash
GET /loki/api/v1/delete
```

List the existing delete requests for the authenticated tenant.
The [log entry deletion](https://grafana.com/docs/loki/<LOKI_VERSION>/operations/storage/logs-deletion/) documentation has configuration details.

Log entry deletion is supported when the TSDB index is configured for the index store. It is also supported on the deprecated BoltDB Shipper index, but BoltDB Shipper is being removed in Loki 4.0, so new deployments should use TSDB.

List the existing delete requests using the following API:

```bash
GET /loki/api/v1/delete
```

This endpoint returns both processed and unprocessed deletion requests. It does not list canceled requests, as those requests will have been removed from storage.

Query parameters:

- `start=<rfc3339 | unix_seconds_timestamp>`: Optional. A timestamp that identifies the start of the time range. Only deletion requests that overlap with this time range will be returned. Must be provided together with `end`.
- `end=<rfc3339 | unix_seconds_timestamp>`: Optional. A timestamp that identifies the end of the time range. Only deletion requests that overlap with this time range will be returned. Must be provided together with `start`.

#### Examples

Example cURL command:

```bash
curl -X GET \
  <COMPACTOR_ADDR>/loki/api/v1/delete \
  -H 'X-Scope-OrgID: <ORG_ID>'
```

The same example deletion request for Grafana Enterprise Logs uses Basic Authentication and specifies the tenant name as a user; `Tenant1` is the tenant name in this example. The password in this example is an access policy token that has been defined in the API_TOKEN environment variable. The token must be for an access policy with `logs:delete` scope for the tenant specified in the user field.

```bash
curl -u "Tenant1:$API_TOKEN" \
  -X GET \
  <COMPACTOR_ADDR>/loki/api/v1/delete
```

### Request cancellation of a delete request

```bash
DELETE /loki/api/v1/delete
```

Remove a delete request for the authenticated tenant.
The [log entry deletion](https://grafana.com/docs/loki/<LOKI_VERSION>/operations/storage/logs-deletion/) documentation has configuration details.

Loki allows cancellation of delete requests until the requests are picked up for processing. It is controlled by the `delete_request_cancel_period` YAML configuration or the equivalent command line option when invoking Loki. To cancel a delete request that has been picked up for processing or is partially complete, pass the `force=true` query parameter to the API.

Log entry deletion is supported when the TSDB index is configured for the index store. It is also supported on the deprecated BoltDB Shipper index, but BoltDB Shipper is being removed in Loki 4.0, so new deployments should use TSDB.

Cancel a delete request using this compactor endpoint:

```bash
DELETE /loki/api/v1/delete
```

Query parameters:

- `request_id=<request_id>`: Identifies the delete request to cancel; IDs are found using the `delete` endpoint.
- `force=<boolean>`: When the `force` query parameter is true, partially completed delete requests will be canceled.
  {{< admonition type="note" >}}
  some data from the request may still be deleted and the deleted request will be listed as 'processed'.
  {{< /admonition >}}

A 204 response indicates success.

#### Examples

Example cURL command:

```bash
curl -X DELETE \
  '<COMPACTOR_ADDR>/loki/api/v1/delete?request_id=<REQUEST_ID>' \
  -H 'X-Scope-OrgID: <TENANT_ID>'
```

The same example deletion cancellation request for Grafana Enterprise Logs uses Basic Authentication and specifies the tenant name as a user; `Tenant1` is the tenant name in this example. The password in this example is an access policy token that has been defined in the API_TOKEN environment variable. The token must be for an access policy with `logs:delete` scope for the tenant specified in the user field.

```bash
curl -u "Tenant1:$API_TOKEN" \
  -X DELETE \
  '<COMPACTOR_ADDR>/loki/api/v1/delete?request_id=<REQUEST_ID>'
```

## Format a LogQL query

```bash
GET /loki/api/v1/format_query
POST /loki/api/v1/format_query
```

The endpoint accepts the following query parameters in the URL:

- `query`: A LogQL query string. Can be passed as URL param (`?query=<query>`) in case of both `GET` and `POST`. Or as form value in case of `POST`.

The `/loki/api/v1/format_query` endpoint lets you format LogQL queries. It returns an error if the passed LogQL is invalid. It is exposed by all Loki components and helps to improve readability and the debugging experience of LogQL queries.

The following example formats the expression LogQL `{foo=   "bar"}` into

```json
{
   "status" : "success",
   "data" : "{foo=\"bar\"}"
}
```

# Loki network ports

Loki components communicate with each other over the network, and clients such as Grafana and Grafana Alloy reach Loki over HTTP.
Use this page to plan firewall rules, Kubernetes NetworkPolicies, or service mesh configuration for a Loki deployment.

All port numbers on this page are defaults.
If you change a port in your configuration, use your value instead.

## Ports that Loki listens on

Every Loki process opens the HTTP and gRPC ports.
Components that share [hash rings](https://grafana.com/docs/loki/<LOKI_VERSION>/get-started/hash-rings/) through memberlist also open the memberlist port.

| Port | Protocol | Configuration parameter | Purpose |
| --- | --- | --- | --- |
| 3100 | HTTP over TCP | `server.http_listen_port` | The [Loki HTTP API](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/loki-http-api/) for pushing and querying logs, the `/metrics` endpoint, and the `/ready` health check endpoint. |
| 9095 | gRPC over TCP | `server.grpc_listen_port` | Requests between Loki components, such as distributors sending logs to ingesters. |
| 7946 | TCP | `memberlist.bind_port` | Gossip messages that keep the hash rings in sync between components. |

Memberlist in Loki sends all gossip traffic over TCP, so you don't need to open UDP for port 7946.

If you enable TLS with `server.http_tls_config`, `server.grpc_tls_config`, or `memberlist.tls_enabled`, the encrypted traffic uses the same ports.
For details, refer to the [`server`](https://grafana.com/docs/loki/<LOKI_VERSION>/configure/#server) and [`memberlist`](https://grafana.com/docs/loki/<LOKI_VERSION>/configure/#memberlist) configuration blocks.

## Connections between components

The following table lists the main connections between Loki components.
In [monolithic and simple scalable modes](https://grafana.com/docs/loki/<LOKI_VERSION>/get-started/deployment-modes/), one process runs several components, but the components still use these ports to reach each other, including across replicas.

| Source | Destination | Port | Protocol | Purpose |
| --- | --- | --- | --- | --- |
| Log collectors, such as Grafana Alloy | Distributor | 3100 | HTTP | Push logs. |
| Grafana and other query clients | Query frontend | 3100 | HTTP | Run queries. |
| Distributor | Ingester | 9095 | gRPC | Send log streams for ingestion. |
| Distributor | Pattern ingester | 9095 | gRPC | Send log lines for pattern detection, when the pattern ingester is enabled. |
| Query frontend | Query scheduler | 9095 | gRPC | Enqueue queries. |
| Query frontend | Querier | 3100 | HTTP | Proxy live tail requests, when `frontend.tail_proxy_url` is set. |
| Querier | Query scheduler | 9095 | gRPC | Pull queries to run. |
| Querier | Query frontend | 9095 | gRPC | Return query results. |
| Querier and ruler | Ingester | 9095 | gRPC | Read recent logs that aren't flushed to object storage yet. |
| Querier and ruler | Index gateway | 9095 | gRPC | Look up the index, when an index gateway is configured. |
| Querier and ruler | Compactor | 9095 or 3100 | gRPC or HTTP | Fetch delete requests, to filter deleted logs out of query results. Loki uses gRPC when `common.compactor_grpc_address` is set, and HTTP when only `common.compactor_address` is set. |
| Index gateway | Bloom gateway | 9095 | gRPC | Filter chunks with bloom filters, when bloom filters are enabled. |
| Bloom builder | Bloom planner | 9095 | gRPC | Receive bloom build tasks, when bloom filters are enabled. |
| Components that use hash rings | Each other | 7946 | TCP | Share hash ring state through memberlist. |
| Ruler | Alertmanager | Alertmanager port, usually 9093 | HTTP | Send alerts. |
| Prometheus or Grafana Alloy | All components | 3100 | HTTP | Scrape the `/metrics` endpoint. |

Loki also connects to services outside the deployment, such as object storage and caches.
Use the ports that those services expose, for example 443 for HTTPS object storage endpoints.

## Additional ports in the Helm chart

The [Loki Helm chart](https://grafana.com/docs/loki/<LOKI_VERSION>/setup/install/helm/concepts/) deploys some components that aren't part of Loki itself.
These components use the following ports:

| Component | Container port | Service port | Protocol | Helm value |
| --- | --- | --- | --- | --- |
| Gateway (NGINX) | 8080 | 80 | HTTP | `gateway.containerPort`, `gateway.service.port` |
| Chunks cache and results cache (Memcached) | 11211 | 11211 | Memcached protocol over TCP | `chunksCache.port`, `resultsCache.port` |
| Memcached exporter | 9150 | 9150 | HTTP | None |
| [Loki Canary](https://grafana.com/docs/loki/<LOKI_VERSION>/operations/loki-canary/) | 3500 | 3500 | HTTP | None |

When the gateway is enabled, clients send push and query requests to the gateway Service on port 80.
The gateway forwards them to the Loki components on port 3100.

Some Services, such as the query frontend Service, also expose port 9096, named `grpclb`.
This port forwards to the gRPC container port, so it doesn't open another port in the pod.

The chart can create NetworkPolicies for this traffic.
To enable them, set `networkPolicy.enabled: true`.
For the related values, such as `networkPolicy.alertmanager.port`, refer to the [Helm chart values](https://grafana.com/docs/loki/<LOKI_VERSION>/setup/install/helm/reference/).

# Query Loki with Python

This page provides Python examples for the most common Loki HTTP API operations: querying logs, pushing log entries, and listing labels. For the full API reference including all parameters and response formats, see the [Loki HTTP API](../loki-http-api/) documentation.

## Prerequisites

Install the [requests](https://requests.readthedocs.io/) library:

```bash
pip install requests
```

If you need an async-capable client, [httpx](https://www.python-httpx.org/) provides a nearly identical API:

```bash
pip install httpx
```

## Authentication

The examples on this page connect to a local Loki instance without authentication. To use these examples with a multi-tenant or Grafana Cloud deployment, add the appropriate authentication as shown below. All functions defined on this page accept `headers`, `auth`, and `verify` parameters for this purpose.

### Multi-tenant mode

If your cluster has [multi-tenancy](../../operations/multi-tenancy/) enabled, pass the tenant ID in the `X-Scope-OrgID` header:

```python
headers = {"X-Scope-OrgID": "my-tenant"}
resp = requests.get(url, params=params, headers=headers)
```

Using the functions defined on this page:

```python
results = query_range(
    url="http://localhost:3100",
    query='{job="varlogs"}',
    start=datetime.now() - timedelta(hours=1),
    end=datetime.now(),
    headers={"X-Scope-OrgID": "my-tenant"},
)
```

To query across multiple tenants, separate tenant names with the pipe (`|`) character:

```python
headers = {"X-Scope-OrgID": "tenant1|tenant2|tenant3"}
```

### Grafana Cloud

For Grafana Cloud, use basic authentication with your Grafana Cloud user and an API token:

```python
resp = requests.get(url, params=params, auth=("<user>", "<API_TOKEN>"))
```

Using the functions defined on this page:

```python
results = query_range(
    url="https://logs-prod-us-central1.grafana.net",
    query='{job="varlogs"}',
    start=datetime.now() - timedelta(hours=1),
    end=datetime.now(),
    auth=("<user>", "<API_TOKEN>"),
)
```

You can find the **User** and **URL** values in the Loki logging service details of your [Grafana Cloud stack](https://grafana.com/docs/grafana-cloud/account-management/cloud-portal/#your-grafana-cloud-stack).

### Self-signed certificates

If your Loki instance uses a self-signed TLS certificate, you can disable certificate verification:

```python
resp = requests.get(url, params=params, verify=False)
```

For production use, pass the path to your CA bundle instead:

```python
resp = requests.get(url, params=params, verify="/path/to/ca-bundle.crt")
```

Using the functions defined on this page:

```python
results = query_range(
    url="https://loki.internal:3100",
    query='{job="varlogs"}',
    start=datetime.now() - timedelta(hours=1),
    end=datetime.now(),
    verify="/path/to/ca-bundle.crt",
)
```

## Query logs within a range of time

[`GET /loki/api/v1/query_range`](../loki-http-api/#query-logs-within-a-range-of-time) queries logs over a time range. This is the most common query operation.

### Using requests

```python
import requests
from datetime import datetime, timedelta


def query_range(
    url: str,
    query: str,
    start: datetime,
    end: datetime,
    limit: int = 1000,
    headers: dict[str, str] | None = None,
    auth: tuple[str, str] | None = None,
    verify: bool | str = True,  # False to skip TLS, or path to CA bundle
) -> list:
    """Query Loki for log entries within a time range."""
    resp = requests.get(
        f"{url}/loki/api/v1/query_range",
        params={
            "query": query,
            "start": str(int(start.timestamp() * 1e9)),  # nanoseconds
            "end": str(int(end.timestamp() * 1e9)),
            "limit": limit,
            "direction": "backward",
        },
        headers=headers,
        auth=auth,
        verify=verify,
    )
    resp.raise_for_status()
    return resp.json()["data"]["result"]


results = query_range(
    url="http://localhost:3100",
    query='{job="varlogs"} |= "error"',
    start=datetime.now() - timedelta(hours=1),
    end=datetime.now(),
)

for stream in results:
    print(f"Labels: {stream['stream']}")
    for ts, line in stream["values"]:
        print(f"  {datetime.fromtimestamp(int(ts) / 1e9)}: {line}")
```

### Using httpx

```python
import httpx
from datetime import datetime, timedelta


def query_range(
    url: str,
    query: str,
    start: datetime,
    end: datetime,
    limit: int = 1000,
    headers: dict[str, str] | None = None,
    auth: tuple[str, str] | None = None,
    verify: bool | str = True,  # False to skip TLS, or path to CA bundle; httpx also accepts ssl.SSLContext
) -> list:
    """Query Loki for log entries within a time range."""
    resp = httpx.get(
        f"{url}/loki/api/v1/query_range",
        params={
            "query": query,
            "start": str(int(start.timestamp() * 1e9)),  # nanoseconds
            "end": str(int(end.timestamp() * 1e9)),
            "limit": limit,
            "direction": "backward",
        },
        headers=headers,
        auth=auth,
        verify=verify,
    )
    resp.raise_for_status()
    return resp.json()["data"]["result"]


results = query_range(
    url="http://localhost:3100",
    query='{job="varlogs"} |= "error"',
    start=datetime.now() - timedelta(hours=1),
    end=datetime.now(),
)

for stream in results:
    print(f"Labels: {stream['stream']}")
    for ts, line in stream["values"]:
        print(f"  {datetime.fromtimestamp(int(ts) / 1e9)}: {line}")
```

## Query logs at a single point in time

[`GET /loki/api/v1/query`](../loki-http-api/#query-logs-at-a-single-point-in-time) evaluates a query at a single point in time. Use this for instant metric queries such as aggregations with `rate()`, `count_over_time()`, or `bytes_over_time()`. Log stream selectors (queries that return log lines) are not supported as instant queries; use `query_range` instead.

```python
import requests
from datetime import datetime


def query_instant(
    url: str,
    query: str,
    headers: dict[str, str] | None = None,
    auth: tuple[str, str] | None = None,
    verify: bool | str = True,  # False to skip TLS, or path to CA bundle
) -> list:
    """Run an instant metric query against Loki."""
    resp = requests.get(
        f"{url}/loki/api/v1/query",
        params={
            "query": query,
            "time": str(int(datetime.now().timestamp() * 1e9)),
        },
        headers=headers,
        auth=auth,
        verify=verify,
    )
    resp.raise_for_status()
    return resp.json()["data"]["result"]


results = query_instant(
    url="http://localhost:3100",
    query='sum(rate({job="varlogs"}[10m])) by (level)',
)

for entry in results:
    print(f"{entry['metric']}: {entry['value'][1]}")
```

## Push logs

[`POST /loki/api/v1/push`](../loki-http-api/#ingest-logs) sends log entries to Loki using the JSON format.

```python
import json
import time
import requests


def push_logs(
    url: str,
    labels: dict[str, str],
    entries: list[tuple[str, str]],
    headers: dict[str, str] | None = None,
    auth: tuple[str, str] | None = None,
    verify: bool | str = True,  # False to skip TLS, or path to CA bundle
) -> None:
    """Push log entries to Loki.

    Args:
        url: Loki base URL.
        labels: Stream labels, for example {"job": "myapp", "env": "dev"}.
        entries: List of (timestamp_ns, log_line) tuples. Use
                 str(int(time.time() * 1e9)) to get a nanosecond timestamp.
    """
    payload = {
        "streams": [
            {
                "stream": labels,
                "values": [list(e) for e in entries],
            }
        ]
    }
    req_headers = {**(headers or {}), "Content-Type": "application/json"}
    resp = requests.post(
        f"{url}/loki/api/v1/push",
        headers=req_headers,
        data=json.dumps(payload),
        auth=auth,
        verify=verify,
    )
    resp.raise_for_status()


now_ns = str(int(time.time() * 1e9))
push_logs(
    url="http://localhost:3100",
    labels={"job": "myapp", "env": "dev"},
    entries=[
        (now_ns, "application started"),
        (now_ns, "listening on port 8080"),
    ],
)
```

## Query labels and label values

[`GET /loki/api/v1/labels`](../loki-http-api/#query-labels) returns the list of known label names. [`GET /loki/api/v1/label/<name>/values`](../loki-http-api/#query-label-values) returns the values for a specific label.

```python
import requests


def get_labels(
    url: str,
    headers: dict[str, str] | None = None,
    auth: tuple[str, str] | None = None,
    verify: bool | str = True,  # False to skip TLS, or path to CA bundle
) -> list[str]:
    """List all known label names."""
    resp = requests.get(
        f"{url}/loki/api/v1/labels",
        headers=headers,
        auth=auth,
        verify=verify,
    )
    resp.raise_for_status()
    return resp.json()["data"]


def get_label_values(
    url: str,
    label: str,
    headers: dict[str, str] | None = None,
    auth: tuple[str, str] | None = None,
    verify: bool | str = True,  # False to skip TLS, or path to CA bundle
) -> list[str]:
    """List values for a specific label."""
    resp = requests.get(
        f"{url}/loki/api/v1/label/{label}/values",
        headers=headers,
        auth=auth,
        verify=verify,
    )
    resp.raise_for_status()
    return resp.json()["data"]


labels = get_labels("http://localhost:3100")
print(f"Labels: {labels}")

for label in labels:
    values = get_label_values("http://localhost:3100", label)
    print(f"  {label}: {values}")
```

## Handling errors

Loki returns standard HTTP status codes. Common errors include:

| Status | Meaning | Typical cause |
|--------|---------|---------------|
| 400 | Bad Request | Invalid LogQL syntax |
| 429 | Too Many Requests | Rate limit exceeded |
| 5xx | Server Error | Loki is unavailable or overloaded |

Use `raise_for_status()` to catch errors, and inspect the response body for details:

```python
import time
import requests


def query_with_retry(
    url: str,
    query: str,
    max_retries: int = 3,
    backoff: float = 1.0,
    headers: dict[str, str] | None = None,
    auth: tuple[str, str] | None = None,
    verify: bool | str = True,  # False to skip TLS, or path to CA bundle
) -> dict:
    """Query Loki with simple retry logic for rate limits."""
    for attempt in range(max_retries):
        resp = requests.get(
            f"{url}/loki/api/v1/query",
            params={"query": query},
            headers=headers,
            auth=auth,
            verify=verify,
        )
        if resp.status_code == 429:
            wait = backoff * (2 ** attempt)
            print(f"Rate limited, retrying in {wait}s...")
            time.sleep(wait)
            continue
        resp.raise_for_status()
        return resp.json()
    raise Exception(f"Query failed after {max_retries} retries")
```

## Common problems

- **Timestamps are in nanoseconds.** Loki expects Unix timestamps in nanoseconds, not seconds or milliseconds. Multiply `time.time()` by `1e9` and convert to a string.
- **At least one label matcher is required.** You cannot query without a stream selector. `{job="myapp"}` works; an empty selector does not.
- **The `direction` parameter changes result ordering.** Use `backward` (the default) to get the most recent entries first, or `forward` to get the oldest entries first.
- **The instant query endpoint only supports metric queries.** Log stream selectors like `{job="myapp"}` return a 400 error on `/query`. Use `/query_range` for log queries, and `/query` for aggregations like `rate()` or `count_over_time()`.
- **Use the `limit` parameter to control result size.** The default is 100 entries. For large time ranges, set a higher limit or paginate by adjusting the `start` parameter based on the last received timestamp.

# TSDB index format v2

Version 2 is Loki's initial TSDB index format. It is written for periods with schema versions `v9` through `v12` and is the closest to the Prometheus TSDB index it was derived from. See [TSDB index format](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/) for the encoding notation used on this page.

## File layout

```
+-----------------------------------------------------------+
| magic(0xBAAAD700) <4b> | version(2) <1b>                  |
+-----------------------------------------------------------+
| symbol table              (TOC.symbols)                   |
+-----------------------------------------------------------+
| series                    (TOC.series)                    |
+-----------------------------------------------------------+
| label indices             (TOC.label_indices)             |
+-----------------------------------------------------------+
| postings                  (TOC.postings)                  |
+-----------------------------------------------------------+
| label offset table        (TOC.label_offset_table)        |
+-----------------------------------------------------------+
| postings offset table     (TOC.postings_offset_table)     |
+-----------------------------------------------------------+
| fingerprint offsets table (TOC.fingerprint_offsets)       |
+-----------------------------------------------------------+
| TOC                       (last 76 bytes of the file)     |
+-----------------------------------------------------------+
```

## Symbol table

All label names and label values are deduplicated into a lexicographically sorted symbol table. Series reference a symbol by its ordinal position in this table, which keeps series entries small. The index decoder keeps the file offset of every 32nd symbol in memory and scans forward from there.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #symbols <4b>                                             |
+-----------------------------------------------------------+
| symbol_0 <uvarint_str>                                    |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| symbol_n-1 <uvarint_str>                                  |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Series

Series are ordered by fingerprint, then by label set. Each entry starts on a 16-byte boundary, and the series reference used by the postings is the entry offset divided by 16.

```
+-----------------------------------------------------------+
| len(entry) <uvarint>                                      |
+-----------------------------------------------------------+
| fingerprint <8b>                                          |
+-----------------------------------------------------------+
| #labels <uvarint>                                         |
+-----------------------------+-----------------------------+
| ref(name_0) <uvarint>       | ref(value_0) <uvarint>      |
+-----------------------------+-----------------------------+
| ...                         | ...                         |
+-----------------------------+-----------------------------+
| ref(name_k-1) <uvarint>     | ref(value_k-1) <uvarint>    |
+-----------------------------+-----------------------------+
| chunks                                                    |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

The stored `fingerprint` is not necessarily `hash(labels)`. Multi-tenant indices carry a synthetic `__loki_tenant__` label whose fingerprint must match the single-tenant fingerprint of the same stream, so the writer takes the fingerprint as an explicit argument.

## Chunks

Chunk metadata entries are sorted by `(min_time, max_time, checksum)` and stored inline in the series entry. `min_time` is delta encoded against the `max_time` of the previous chunk, so it is written as a signed `varint` because chunks may overlap. The first chunk's delta is relative to 0.

```
+-----------------------------------------------------------+
| #chunks <uvarint>                                         |
+-----------------------------------------------------------+
| chunk_0:                                                  |
|   min_time delta <varint>       (vs. previous max_time)   |
|   max_time - min_time <uvarint>                           |
|   KB <uvarint>                  (size, rounded to KB)     |
|   #entries <uvarint>                                      |
|   checksum <4b>                 (chunk identity)          |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| chunk_n-1: (same fields)                                  |
+-----------------------------------------------------------+
```

`KB` and `#entries` are Loki additions used by the query frontend to plan [dynamic query sharding](https://grafana.com/docs/loki/<LOKI_VERSION>/operations/storage/tsdb/#dynamic-query-sharding). Because the metas form a single delta chain, computing statistics for a time range requires decoding chunks from the start of the chain.

## Label indices

One entry per label name, holding the symbol references of all values seen for that name. Entries are 4-byte aligned.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #names <4b>                     (always 1)                |
+-----------------------------------------------------------+
| #entries <4b>                                             |
+-----------------------------------------------------------+
| ref(value_0) <4b>                                         |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Postings

One sorted list of series references per label name/value pair, 4-byte aligned. The pair `("", "")` holds all series in the index.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #entries <4b>                                             |
+-----------------------------------------------------------+
| series_ref_0 <4b>                                         |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Label offset table

Maps a label name to the file offset of its label index entry.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #entries <4b>                                             |
+-----------------------------------------------------------+
| entry_0:                                                  |
|   #names <uvarint>              (always 1)                |
|   name <uvarint_str>                                      |
|   offset <uvarint>                                        |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Postings offset table

Maps a label name/value pair to the file offset of its postings list.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #entries <4b>                                             |
+-----------------------------------------------------------+
| entry_0:                                                  |
|   #keys <uvarint>               (always 2)                |
|   name <uvarint_str>                                      |
|   value <uvarint_str>                                     |
|   offset <uvarint>                                        |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Fingerprint offsets table

A Loki-specific sampled index from fingerprint to series reference, recorded for every 1024th series. It lets a querier seek directly into the series section for a fingerprint range instead of walking the postings, which is what makes fingerprint-bounded shard queries cheap.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #entries <4b>                                             |
+-----------------------------+-----------------------------+
| series_ref_0 <8b>           | fingerprint_0 <8b>          |
+-----------------------------+-----------------------------+
| ...                         | ...                         |
+-----------------------------+-----------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## TOC

The table of contents occupies the last 76 bytes of the file. Readers parse it first and then address every other section by offset. An offset of 0 means the section is empty.

```
+-----------------------------------------------------------+
| symbols <8b>                                              |
+-----------------------------------------------------------+
| series <8b>                                               |
+-----------------------------------------------------------+
| label_indices <8b>                                        |
+-----------------------------------------------------------+
| label_offset_table <8b>                                   |
+-----------------------------------------------------------+
| postings <8b>                                             |
+-----------------------------------------------------------+
| postings_offset_table <8b>                                |
+-----------------------------------------------------------+
| fingerprint_offsets <8b>                                  |
+-----------------------------------------------------------+
| metadata.from <8b>              (oldest chunk min_time)   |
+-----------------------------------------------------------+
| metadata.through <8b>           (newest chunk max_time)   |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

`metadata.from` and `metadata.through` are Loki additions. They expose the time range covered by the index file, so a querier can discard an entire index without reading any of its sections.

## Differences from the Prometheus index

- Series entries store an explicit 8-byte fingerprint.
- Chunk metas store `KB`, `#entries`, and the chunk checksum instead of a chunk file reference, because Loki chunks are addressed in object storage by series fingerprint and checksum.
- The index adds a fingerprint offsets table and the `from`/`through` metadata in the TOC.

# TSDB index format v3

Version 3 is written for periods with schema version `v13`. It is identical to [version 2](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/) except for the chunks part of a series entry, which gains page markers so that readers can skip over chunks that fall outside the queried time range. See [TSDB index format](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/) for the encoding notation used on this page.

## Differences from v2

- The chunks part of a series entry is prefixed with a markers section: `len(markers)`, `#markers`, and one marker per page of 16 chunks.
- Chunk statistics for a time range can be answered from the markers, without decoding individual chunk metas.

## File layout

```
+-----------------------------------------------------------+
| magic(0xBAAAD700) <4b> | version(3) <1b>                  |
+-----------------------------------------------------------+
| symbol table              (TOC.symbols)                   |
+-----------------------------------------------------------+
| series                    (TOC.series)                    |
+-----------------------------------------------------------+
| label indices             (TOC.label_indices)             |
+-----------------------------------------------------------+
| postings                  (TOC.postings)                  |
+-----------------------------------------------------------+
| label offset table        (TOC.label_offset_table)        |
+-----------------------------------------------------------+
| postings offset table     (TOC.postings_offset_table)     |
+-----------------------------------------------------------+
| fingerprint offsets table (TOC.fingerprint_offsets)       |
+-----------------------------------------------------------+
| TOC                       (last 76 bytes of the file)     |
+-----------------------------------------------------------+
```

## Symbol table

Unchanged from v2: a lexicographically sorted, deduplicated table of all label names and values, referenced by ordinal position.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #symbols <4b>                                             |
+-----------------------------------------------------------+
| symbol_0 <uvarint_str>                                    |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Series

The series header is unchanged from v2. Entries start on a 16-byte boundary and the series reference is the entry offset divided by 16.

```
+-----------------------------------------------------------+
| len(entry) <uvarint>                                      |
+-----------------------------------------------------------+
| fingerprint <8b>                                          |
+-----------------------------------------------------------+
| #labels <uvarint>                                         |
+-----------------------------+-----------------------------+
| ref(name_0) <uvarint>       | ref(value_0) <uvarint>      |
+-----------------------------+-----------------------------+
| ...                         | ...                         |
+-----------------------------+-----------------------------+
| chunks                          (see below, v3 layout)    |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Chunks

The chunks part is split into a markers section and the chunk metas. Each marker summarizes a fixed-size page of chunks: its aggregated size and entry count, its time bounds, and the byte offset at which the page starts.

```
+-----------------------------------------------------------+
| #chunks <uvarint>                                         |
+-----------------------------------------------------------+
| len(markers) <4b>               (skip to chunk metas)     |
+-----------------------------------------------------------+
| #markers <uvarint>                                        |
+-----------------------------------------------------------+
| marker_0:                                                 |
|   #chunks in page <uvarint>                               |
|   KB <4b>                       (sum over page)           |
|   #entries <4b>                 (sum over page)           |
|   offset <uvarint>              (page start, relative to  |
|                                  the first chunk meta)    |
|   min_time <varint>             (page bounds)             |
|   max_time - min_time <varint>                            |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| marker_m-1: (same fields, may cover a partial page)       |
+-----------------------------------------------------------+
| chunk_0:                                                  |
|   min_time delta <varint>       (vs. previous max_time)   |
|   max_time - min_time <uvarint>                           |
|   KB <uvarint>                                            |
|   #entries <uvarint>                                      |
|   checksum <4b>                                           |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| chunk_n-1: (same fields)                                  |
+-----------------------------------------------------------+
```

The chunk meta encoding itself is byte-for-byte the same as in v2.

How readers use the markers:

- A page holds 16 chunks. The page size is recoverable from each marker's `#chunks in page` field, so it can be changed without a new index version.
- Series with fewer than 64 chunks skip the markers entirely, using `len(markers)`, because a linear scan is faster at low chunk counts.
- If a page is fully contained in the queried time range, the aggregated `KB` and `#entries` of its marker are used directly and no chunk meta is decoded. Partially overlapping pages are decoded chunk by chunk, and pages that start after the query end terminate the scan.
- The `min_time` delta chain of the chunk metas does not reset at page boundaries. When a reader jumps to a page via its `offset`, it discards the first chunk's delta and uses the page's `min_time` instead.

## Remaining sections

The label indices, postings, label offset table, postings offset table, fingerprint offsets table, and TOC are identical to v2. See:

- [Label indices](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#label-indices)
- [Postings](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#postings)
- [Label offset table](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#label-offset-table)
- [Postings offset table](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#postings-offset-table)
- [Fingerprint offsets table](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#fingerprint-offsets-table)
- [TOC](../v2/#toc)

# TSDB index format v4

Version 4 is written for periods with schema version `v14`. This version is experimental. It keeps the paging layout of [version 3](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v3/) and appends an ingestion timestamp to every chunk meta, which allows retention to be measured from the time a chunk was ingested rather than from the timestamps of the log lines it contains. See [TSDB index format](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/) for the encoding notation used on this page.

## Differences from v3

- Each chunk meta ends with an `ingested_at delta` field, encoded at day precision relative to `max_time`.
- The field is 0 for all chunks that were not backfilled.

## File layout

```
+-----------------------------------------------------------+
| magic(0xBAAAD700) <4b> | version(4) <1b>                  |
+-----------------------------------------------------------+
| symbol table              (TOC.symbols)                   |
+-----------------------------------------------------------+
| series                    (TOC.series)                    |
+-----------------------------------------------------------+
| label indices             (TOC.label_indices)             |
+-----------------------------------------------------------+
| postings                  (TOC.postings)                  |
+-----------------------------------------------------------+
| label offset table        (TOC.label_offset_table)        |
+-----------------------------------------------------------+
| postings offset table     (TOC.postings_offset_table)     |
+-----------------------------------------------------------+
| fingerprint offsets table (TOC.fingerprint_offsets)       |
+-----------------------------------------------------------+
| TOC                       (last 76 bytes of the file)     |
+-----------------------------------------------------------+
```

## Symbol table

Unchanged from v2 and v3: a lexicographically sorted, deduplicated table of all label names and values, referenced by ordinal position.

```
+-----------------------------------------------------------+
| len <4b>                                                  |
+-----------------------------------------------------------+
| #symbols <4b>                                             |
+-----------------------------------------------------------+
| symbol_0 <uvarint_str>                                    |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Series

The series header is unchanged. Entries start on a 16-byte boundary and the series reference is the entry offset divided by 16.

```
+-----------------------------------------------------------+
| len(entry) <uvarint>                                      |
+-----------------------------------------------------------+
| fingerprint <8b>                                          |
+-----------------------------------------------------------+
| #labels <uvarint>                                         |
+-----------------------------+-----------------------------+
| ref(name_0) <uvarint>       | ref(value_0) <uvarint>      |
+-----------------------------+-----------------------------+
| ...                         | ...                         |
+-----------------------------+-----------------------------+
| chunks                          (see below, v4 layout)    |
+-----------------------------------------------------------+
| CRC32 <4b>                                                |
+-----------------------------------------------------------+
```

## Chunks

The markers section is identical to v3. Each chunk meta carries one additional trailing field, `ingested_at delta`.

```
+-----------------------------------------------------------+
| #chunks <uvarint>                                         |
+-----------------------------------------------------------+
| len(markers) <4b>               (skip to chunk metas)     |
+-----------------------------------------------------------+
| #markers <uvarint>                                        |
+-----------------------------------------------------------+
| marker_0:                                                 |
|   #chunks in page <uvarint>                               |
|   KB <4b>                       (sum over page)           |
|   #entries <4b>                 (sum over page)           |
|   offset <uvarint>              (page start, relative to  |
|                                  the first chunk meta)    |
|   min_time <varint>             (page bounds)             |
|   max_time - min_time <varint>                            |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| marker_m-1: (same fields, may cover a partial page)       |
+-----------------------------------------------------------+
| chunk_0:                                                  |
|   min_time delta <varint>       (vs. previous max_time)   |
|   max_time - min_time <uvarint>                           |
|   KB <uvarint>                                            |
|   #entries <uvarint>                                      |
|   checksum <4b>                                           |
|   ingested_at delta <uvarint>   (v4 only, see below)      |
+-----------------------------------------------------------+
| ...                                                       |
+-----------------------------------------------------------+
| chunk_n-1: (same fields)                                  |
+-----------------------------------------------------------+
```

Because the field is appended to the chunk meta, a v4 chunk meta cannot be read by a v3 reader. The index version in the header selects the decoder, so a cluster only needs to know that indices written under schema `v14` cannot be read by a version of Loki that predates v4 support.

### Ingestion timestamp encoding

`ingested_at` records when a chunk was flushed to storage, not when its log lines were received. It is only recorded for backfilled chunks, that is, chunks of a stream carrying the `__backfill__="true"` label. It is stored at day precision, relative to the chunk's `max_time`:

```
day               = 86400000 ms
delta_days        = ceil_day(ingested_at) - floor_day(max_time)
ingested_at delta = zigzag(delta_days) + 1
```

- The value 0 is a sentinel meaning "no ingestion timestamp". Every non-backfilled chunk stores 0, so the field costs a single byte for the common case.
- `ingested_at` is rounded up to the next UTC day boundary. Rounding must never move the timestamp earlier, otherwise retention measured from ingestion could expire a chunk before its window elapsed. The cost is retaining a chunk for up to one day longer.
- The zigzag mapping keeps small negative deltas cheap: 0 days encodes as 0, -1 day as 1, +1 day as 2. Adding 1 keeps the encoded 0 free for the sentinel.
- Rounding is stable. Re-encoding an already rounded `ingested_at` yields the same delta, so compaction does not drift the value.

For example, if `max_time` is 2026-01-10 14:30 UTC and the chunk was ingested at 2026-01-12 08:00 UTC, ingestion rounds up to 2026-01-13 00:00 UTC, the day of `max_time` is 2026-01-10, and the stored delta is +3 days.

## Remaining sections

The label indices, postings, label offset table, postings offset table, fingerprint offsets table, and TOC are identical to v2 and v3. See:

- [Label indices](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#label-indices)
- [Postings](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#postings)
- [Label offset table](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#label-offset-table)
- [Postings offset table](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#postings-offset-table)
- [Fingerprint offsets table](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#fingerprint-offsets-table)
- [TOC](https://grafana.com/docs/loki/<LOKI_VERSION>/reference/tsdb-index-format/v2/#toc)