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

  1. 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 just fmt.Println or 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 JsonLayout or JsonEncoder to your logback.xml configuration.
      • 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' });
        
    • Why it works: JSON formaters serialize your log data into a key-value structure. Libraries like logrus and zap are designed to accept structured data (Fields or Fields arguments) and automatically include them as key-value pairs in the JSON output.
  2. 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_stages of your promtail-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 json stage parses the incoming log line as JSON. The expressions map which JSON keys to extract. The labels stage then takes those extracted values and turns them into Loki labels, which are indexed for fast querying. You can filter using level="info" or user_id="alice".
  3. Inconsistent JSON Schemas:

    • Diagnosis: Your application sometimes logs a user_id and sometimes logs userId. Or, it might log error_code in one instance and errorCode in another. This leads to duplicate labels in Loki (e.g., user_id="alice" and userId="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_id is always user_id, the labels stage in Promtail will always create the user_id label with the correct value.
  4. Log Levels Not Explicitly Captured:

    • Diagnosis: Your application logs messages like INFO: User logged in but the INFO part is just text within the message, not a dedicated field. This makes it hard to filter for all INFO level 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 JsonFormatter usually captures levelname.
    • Why it works: Having a dedicated level field allows you to filter logs easily, e.g., level="error" or level="warn".
  5. 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): JsonFormatter can be configured to use ISO 8601.
      • Promtail: Ensure Promtail’s timestamp stage is configured correctly if your application doesn’t log a standard timestamp. However, it’s best practice for the application to log it correctly.
    • 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. The Z denotes UTC.
  6. 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.HideKey or similar mechanisms if your library supports them.
      // 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 name
      
      A more direct approach is to carefully select what you log and ensure fields like password are 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.

Want structured learning?

Take the full Observability & Monitoring course →