Prometheus doesn’t actually do structured logging itself; it’s a time-series database and monitoring system. What you’re likely hitting is the challenge of getting your application logs into a format that Prometheus, or more accurately, systems that integrate with Prometheus (like Grafana Loki), can effectively parse and query. The core problem is that Prometheus expects metrics, not logs, and while Loki can ingest logs, it thrives on well-structured data.
Let’s say you’re trying to use Grafana Loki to collect logs from your applications, and you’re finding it hard to query specific events or aggregate them meaningfully. This usually happens because your application logs are a chaotic mix of free-form text, JSON fragments, and perhaps some semi-structured lines that are difficult to parse consistently.
Here’s why that’s a problem and how to fix it:
The Problem: Inconsistent Log Formats
When logs are just plain text, searching for specific events becomes a nightmare. Imagine trying to find all instances of a particular error message when the message itself changes slightly from line to line, or when key details like user IDs or request IDs are embedded inconsistently. Loki, and by extension Prometheus’s ecosystem, relies on labels for efficient filtering and aggregation. Without consistent, parseable fields in your logs, you’re essentially blind.
The Solution: Structured Logging
Structured logging means emitting logs as machine-readable data, typically JSON. Instead of:
2023-10-27 10:30:00 INFO User 'alice' logged in from 192.168.1.100
You’d have something like:
{
"timestamp": "2023-10-27T10:30:00Z",
"level": "info",
"message": "User logged in",
"user_id": "alice",
"ip_address": "192.168.1.100"
}
This JSON structure makes it trivial for log aggregation systems like Loki to extract fields and use them as labels.
Common Causes and Fixes
-
Application Emitting Plain Text Logs:
- Diagnosis: Check your application’s logging configuration. Are you using a standard library like
log4j,logrus,zap,Python's logging? Look for settings that control the output format. If it’s justfmt.Printlnor simple string concatenation for logs, it’s likely plain text. - Fix: Configure your logging library to output JSON.
- Go (logrus):
log := logrus.New() log.SetFormatter(&logrus.JSONFormatter{}) log.Info("User logged in", logrus.Fields{"user_id": "alice"}) - Go (zap):
logger, _ := zap.NewProduction() // or NewDevelopment() logger.Info("User logged in", zap.String("user_id", "alice"), zap.String("ip_address", "192.168.1.100"), ) - Python (logging with jsonformatter):
import logging from pythonjsonlogger import jsonlogger logger = logging.getLogger('my_app') logger.setLevel(logging.INFO) handler = logging.StreamHandler() formatter = jsonlogger.JsonFormatter() handler.setFormatter(formatter) logger.addHandler(handler) logger.info('User logged in', extra={'user_id': 'alice', 'ip_address': '192.168.1.100'}) - Java (Logback): Add a
JsonLayoutorJsonEncoderto yourlogback.xmlconfiguration. - Node.js (Winston):
const { createLogger, format, transports } = require('winston'); const logger = createLogger({ level: 'info', format: format.combine( format.timestamp(), format.json() ), transports: [new transports.Console()], }); logger.info('User logged in', { user_id: 'alice', ip_address: '192.168.1.100' });
- Go (logrus):
- Why it works: JSON formaters serialize your log data into a key-value structure. Libraries like
logrusandzapare designed to accept structured data (FieldsorFieldsarguments) and automatically include them as key-value pairs in the JSON output.
- Diagnosis: Check your application’s logging configuration. Are you using a standard library like
-
Log Agent Not Extracting Fields:
- Diagnosis: You’ve got JSON logs, but when you query in Loki, you can’t filter by
user_id. This means your log agent (like Promtail, Fluentd, Fluent Bit) isn’t configured to parse the JSON and expose its fields as Loki labels. - Fix: Configure your log agent to parse the JSON and extract specific fields as labels. For Promtail, this is done in the
pipeline_stagesof yourpromtail-config.yaml.scrape_configs: - job_name: myapp static_configs: - targets: - localhost labels: job: myapp __path__: /var/log/myapp/*.log pipeline_stages: - json: expressions: level: message: user_id: ip_address: - labels: level: user_id: ip_address: - Why it works: The
jsonstage parses the incoming log line as JSON. Theexpressionsmap which JSON keys to extract. Thelabelsstage then takes those extracted values and turns them into Loki labels, which are indexed for fast querying. You can filter usinglevel="info"oruser_id="alice".
- Diagnosis: You’ve got JSON logs, but when you query in Loki, you can’t filter by
-
Inconsistent JSON Schemas:
- Diagnosis: Your application sometimes logs a
user_idand sometimes logsuserId. Or, it might logerror_codein one instance anderrorCodein another. This leads to duplicate labels in Loki (e.g.,user_id="alice"anduserId="bob") making it impossible to query consistently. - Fix: Standardize your JSON field names across your entire application codebase. Use consistent casing (e.g., snake_case or camelCase) and always use the same key for the same piece of information. If you have multiple services, enforce this standard via documentation or code reviews.
- Why it works: Consistency in field names ensures that the log agent can reliably map JSON keys to Loki labels. If
user_idis alwaysuser_id, thelabelsstage in Promtail will always create theuser_idlabel with the correct value.
- Diagnosis: Your application sometimes logs a
-
Log Levels Not Explicitly Captured:
- Diagnosis: Your application logs messages like
INFO: User logged inbut theINFOpart is just text within the message, not a dedicated field. This makes it hard to filter for allINFOlevel messages. - Fix: Ensure your structured logging library automatically captures the log level as a distinct field. Most JSON formatters do this by default. If not, explicitly add it.
- Go (zap):
logger.Info("User logged in", zap.String("user_id", "alice"))automatically adds"level": "info". - Python (jsonlogger): The
JsonFormatterusually captureslevelname.
- Go (zap):
- Why it works: Having a dedicated
levelfield allows you to filter logs easily, e.g.,level="error"orlevel="warn".
- Diagnosis: Your application logs messages like
-
Timestamps in Wrong Format or Timezone:
- Diagnosis: When you query logs, the timestamps seem off, or you can’t correlate them easily with events in other systems. This often happens if timestamps are logged as strings in local time or in an ambiguous format.
- Fix: Log timestamps in ISO 8601 format with UTC timezone. Most structured logging libraries support this.
- Go (zap):
zap.NewProduction()uses ISO 8601 UTC by default. - Python (jsonlogger):
JsonFormattercan be configured to use ISO 8601. - Promtail: Ensure Promtail’s
timestampstage is configured correctly if your application doesn’t log a standard timestamp. However, it’s best practice for the application to log it correctly.
- Go (zap):
- Why it works: ISO 8601 (e.g.,
2023-10-27T10:30:00Z) is unambiguous and universally understood, making it easy for Loki and other systems to parse and sort logs chronologically. TheZdenotes UTC.
-
Sensitive Data in Log Fields (Indirectly a Formatting Issue):
- Diagnosis: You’re logging sensitive data like passwords or PII, and you realize you can’t easily query for non-sensitive information because the sensitive data pollutes your query results or violates privacy policies.
- Fix: Design your structured logs to exclude sensitive fields by default. If you need to log specific sensitive data for debugging, do so with explicit intent and potentially in a separate, more secured log stream or with specific redaction logic. Use
zapcore.HideKeyor similar mechanisms if your library supports them.
A more direct approach is to carefully select what you log and ensure fields like// Example with zap sensitiveEncoderConfig := zap.NewProductionEncoderConfig() sensitiveEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder sensitiveEncoderConfig.NameEncoder = zapcore.Encode selamaName // Prevents encoding sensitive keys sensitiveLogger, _ := zap.New(zapcore.NewCore( zapcore.NewJSONEncoder(sensitiveEncoderConfig), zapcore.Lock(os.Stdout), zapcore.InfoLevel, )) sensitiveLogger.Info("User login attempt", zap.String("user_id", "alice")) // "user_id" is not encoded if it were a sensitive key namepasswordare never passed to the logger. - Why it works: By controlling which fields are logged and how they are encoded, you maintain data privacy and ensure that your log queries are focused on actionable, non-sensitive information.
By adopting structured logging and ensuring your log agents are configured to leverage it, you transform your application logs from a text dump into a powerful, queryable dataset that integrates seamlessly with the Prometheus ecosystem.
The next step after ensuring consistent, structured logs is often learning how to create alerts based on log patterns using Loki’s alerting rules.