Log Service
The api-gateway exposes log ingestion endpoints that forward structured log entries to Loki for centralized observability.
| Endpoint | Consumer | Loki job label |
|---|---|---|
POST /v1/logs | Web / frontend | frontend_app |
POST /v1/logs/mobile | Mobile app | mobile_app |
Base URLs
| Environment | URL |
|---|---|
| Staging | https://gw-staging.uts.network |
| Production | https://gw.uts.network |
POST /v1/logs
Ingest a log entry from a web / frontend client.
Authentication
Validates the request by resolving the user ID from the GraphQL profile service using the provided Authorization header. Returns 401 Unauthorized if the user cannot be resolved.
Authorization Header
Authorization: <user-token>
Request body
Request body
{
"message": "User tapped checkout",
"level": "INFO",
"trade_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"timestamp": 1748649600000,
"labels": {
"app": "xperise-web",
"platform": "web",
"env": "production"
},
"data": {
"screen": "checkout"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Human-readable log message. |
level | string | Yes | Severity: DEBUG, INFO, WARN, ERROR. |
trade_id | string | No | Trace / correlation ID for distributed tracing. |
timestamp | number | No | Unix timestamp in milliseconds. Defaults to server time when omitted or 0. |
labels | object | No | Key-value pairs added as Loki labels or metadata (see Label routing). |
data | any JSON | No | Arbitrary structured payload stored in the log line. |
cURL example
note
This endpoint only accepts POST. Running curl without -X POST sends a GET request and returns 404.
cURL — POST /v1/logs
curl -X POST https://gw-staging.uts.network/v1/logs \
-H "Content-Type: application/json" \
-H "Authorization: <user-token>" \
-d '{
"message": "User tapped checkout",
"level": "INFO",
"timestamp": 1748649600000,
"labels": {
"app": "xperise-web",
"platform": "web",
"env": "staging"
},
"data": {
"screen": "checkout"
}
}'
TypeScript example
sendLog.ts
async function sendLog(
message: string,
level: "DEBUG" | "INFO" | "WARN" | "ERROR",
data?: Record<string, unknown>
) {
await fetch("https://gw.uts.network/v1/logs", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: userToken,
},
body: JSON.stringify({
message,
level,
timestamp: Date.now(),
labels: {
app: "xperise-web",
platform: "web",
env: import.meta.env.MODE,
},
data,
}),
});
}
// Usage
sendLog("Payment failed", "ERROR", { code: "CARD_DECLINED", amount: 150000 });
Error responses
| Status | Body | Reason |
|---|---|---|
400 | {"error":"message is required"} | message field missing. |
400 | {"error":"level is required"} | level field missing. |
400 | {"error":"Invalid JSON payload"} | Request body is not valid JSON. |
401 | {"error":"Unauthorized"} | Token missing or user could not be resolved. |
500 | {"error":"Failed to push log"} | Loki push failed. |
POST /v1/logs/mobile
Ingest a log entry from a mobile client.
Authentication
Uses a daily-rotating bearer token. Contact the backend team to obtain the token generation mechanism.
Authorization Header
Authorization: Bearer <daily-token>
Request body
Same schema as POST /v1/logs.
cURL example
cURL — POST /v1/logs/mobile
curl -X POST https://gw-staging.uts.network/v1/logs/mobile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <daily-token>" \
-d '{
"message": "App launched",
"level": "INFO",
"timestamp": 1748649600000,
"labels": {
"app": "xperise-mobile",
"platform": "ios",
"env": "staging"
},
"data": {
"screen": "home"
}
}'
Error responses
| Status | Body | Reason |
|---|---|---|
400 | {"error":"message is required"} | message field missing. |
400 | {"error":"level is required"} | level field missing. |
400 | {"error":"Invalid JSON payload"} | Request body is not valid JSON. |
401 | {"error":"Missing or invalid Authorization header"} | Bearer token missing. |
401 | {"error":"Unauthorized"} | Token does not match today's expected value. |
500 | {"error":"Failed to push log"} | Loki push failed. |
Label routing
Low-cardinality keys are indexed as Loki stream labels; everything else is stored as metadata inside the JSON log line.
| Label key | Indexed in Loki |
|---|---|
app | Yes |
platform | Yes |
env | Yes |
level | Yes |
job | Yes |
| anything else | No (stored in metadata) |
Querying logs in Grafana / Loki
Grafana / Loki
# All frontend errors
{job="frontend_app", level="ERROR"}
# All mobile errors
{job="mobile_app", level="ERROR"}
# Search by message content
{job="frontend_app"} |= "Payment failed"
# Filter by trace ID in the log line
{job="frontend_app"} | json | trace_id="4bf92f3577b34da6a3ce929d0e0e4736"
# Filter by user
{job="frontend_app"} | json | user_id="42"