This is documentation for the next version of Grafana Loki documentation. For the latest stable release, go to the latest version.
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 documentation.
Prerequisites
Install the requests library:
pip install requestsIf you need an async-capable client, httpx provides a nearly identical API:
pip install httpxAuthentication
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.
Multi-tenant mode
If your cluster has multi-tenancy enabled, pass the tenant ID in the X-Scope-OrgID header:
headers = {"X-Scope-OrgID": "my-tenant"}
resp = requests.get(url, params=params, headers=headers)To query across multiple tenants, separate tenant names with the pipe (|) character:
headers = {"X-Scope-OrgID": "tenant1|tenant2|tenant3"}Grafana Cloud
For Grafana Cloud, use basic authentication with your Grafana Cloud user and an API token:
resp = requests.get(url, params=params, auth=("<user>", "<API_TOKEN>"))You can find the User and URL values in the Loki logging service details of your Grafana Cloud stack.
Self-signed certificates
If your Loki instance uses a self-signed TLS certificate, you can disable certificate verification:
resp = requests.get(url, params=params, verify=False)For production use, pass the path to your CA bundle instead:
resp = requests.get(url, params=params, verify="/path/to/ca-bundle.crt")Query logs within a range of time
GET /loki/api/v1/query_range queries logs over a time range. This is the most common query operation.
Using requests
import requests
from datetime import datetime, timedelta
def query_range(
url: str,
query: str,
start: datetime,
end: datetime,
limit: int = 1000,
) -> 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",
},
)
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
import httpx
from datetime import datetime, timedelta
def query_range(
url: str,
query: str,
start: datetime,
end: datetime,
limit: int = 1000,
) -> 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",
},
)
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 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.
import requests
from datetime import datetime
def query_instant(url: str, query: str) -> 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)),
},
)
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 sends log entries to Loki using the JSON format.
import json
import time
import requests
def push_logs(
url: str,
labels: dict[str, str],
entries: list[tuple[str, str]],
) -> 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],
}
]
}
resp = requests.post(
f"{url}/loki/api/v1/push",
headers={"Content-Type": "application/json"},
data=json.dumps(payload),
)
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 returns the list of known label names. GET /loki/api/v1/label/<name>/values returns the values for a specific label.
import requests
def get_labels(url: str) -> list[str]:
"""List all known label names."""
resp = requests.get(f"{url}/loki/api/v1/labels")
resp.raise_for_status()
return resp.json()["data"]
def get_label_values(url: str, label: str) -> list[str]:
"""List values for a specific label."""
resp = requests.get(f"{url}/loki/api/v1/label/{label}/values")
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:
Use raise_for_status() to catch errors, and inspect the response body for details:
import time
import requests
def query_with_retry(
url: str,
query: str,
max_retries: int = 3,
backoff: float = 1.0,
) -> 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},
)
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()by1e9and 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
directionparameter changes result ordering. Usebackward(the default) to get the most recent entries first, orforwardto 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_rangefor log queries, and/queryfor aggregations likerate()orcount_over_time(). - Use the
limitparameter to control result size. The default is 100 entries. For large time ranges, set a higher limit or paginate by adjusting thestartparameter based on the last received timestamp.



