Terraform Provider

The LogStruct Terraform provider builds CloudWatch Logs filter patterns for LogStruct's log lines from a source (src) and an event (evt). The plan fails if no LogStruct line has that source and event, so a typo or a removed event is caught before anything is deployed.

Installation

Add the provider to required_providers. It has no settings, so it needs no provider block.

versions.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
    }
    logstruct = {
      source  = "DocSpring/logstruct"
      version = "~> 0.2.0"
    }
  }
}

The AWS provider is only needed for the module below, which creates an aws_cloudwatch_log_metric_filter. The module asks for the LogStruct provider >= 0.1.0, so your constraint above decides the version.

CloudWatch Metric Filter

The metric-filter module counts the lines with a source and an event: each matching line adds 1 to the metric. Its inputs are name, log_group_name, log_source, log_event, namespace and an optional metric_name (by default <log_source>_<log_event>, here mailer_delivered). It outputs the pattern it used.

Module: Metric Filter
module "email_delivered_count" {
  source  = "DocSpring/logstruct/aws//modules/metric-filter"
  version = "~> 0.1.0"

  name           = "Email Delivered Count"
  log_group_name = var.log_group_name
  log_source     = "mailer"
  log_event      = "delivered"
  namespace      = var.namespace
}
Variables
variable "log_group_name" { type = string }
variable "namespace" { type = string }

What the Provider Checks

  • The provider has a catalog of LogStruct's sources and the events each one writes, compiled into the provider. Each provider release is built from the LogStruct release with the same version number.
  • It never looks at your app or the gem version your app runs. Upgrading the gem changes nothing in Terraform; a plan can only start failing when you upgrade the provider and your source and event are no longer in its catalog.
  • An unknown source or event fails the plan:
    Error: Invalid event
    event deliverd is not allowed for source mailer
    
    Error: Unknown source
    No structs found with fixed source = payments
  • The pattern matches src and evt only, for example { $.evt = "delivered" && $.src = "mailer" }. Lines from different integrations with the same source and event match the same pattern: ActiveJob's and GoodJob's error lines both have src: "job" and evt: "error".
  • The catalog only has sources with a fixed set of events, so it has no pattern for the general error lines (such as src: "app", evt: "error") or for your own custom log types. Write those patterns yourself, as in the last recipe below.

Versioning

  • Provider versions match LogStruct's: provider 0.2.0 has the sources and events of LogStruct 0.2.0.
  • Use ~> 0.2.0 to get 0.2.x releases only. Below 1.0, ~> 0.2 also allows 0.3 and later, and a minor release can remove events: 0.2.0 removed GoodJob's enqueue, start, finish and schedule, which GoodJob never wrote (its jobs' lines come from ActiveJob, with the same source and events).

Recipes

Count email deliveries

module "email_delivered_count" {
  source  = "DocSpring/logstruct/aws//modules/metric-filter"
  version = "~> 0.1.0"

  name           = "Email Delivered Count"
  log_group_name = var.log_group_name
  log_source     = "mailer"
  log_event      = "delivered"
  namespace      = var.namespace
}

Count mailer errors

LogStruct writes a mailer error line when a mailer raises an error, as when a delivery fails.

module "mailer_error_count" {
  source  = "DocSpring/logstruct/aws//modules/metric-filter"
  version = "~> 0.1.0"

  name           = "Mailer Error Count"
  log_group_name = var.log_group_name
  log_source     = "mailer"
  log_event      = "error"
  namespace      = var.namespace
}

Count finished ActiveJob runs

ActiveJob writes a finish line when a job's perform returns. That includes a run whose exception retry_on or discard_on handled, which also has a retry or discard line. A run that raises has an error line instead.

module "job_finish_count" {
  source  = "DocSpring/logstruct/aws//modules/metric-filter"
  version = "~> 0.1.0"

  name           = "ActiveJob Finish Count"
  log_group_name = var.log_group_name
  log_source     = "job"
  log_event      = "finish"
  namespace      = var.namespace
}

Count slow SQL queries

SQL lines are off by default (enable_sql_logging). When they're on, LogStruct writes one for each query that takes at least sql_slow_query_threshold milliseconds (100 by default).

module "slow_sql_count" {
  source  = "DocSpring/logstruct/aws//modules/metric-filter"
  version = "~> 0.1.0"

  name           = "Slow SQL Query Count"
  log_group_name = var.log_group_name
  log_source     = "app"
  log_event      = "database"
  namespace      = var.namespace
}

Use the data sources directly

The module uses the provider's two data sources. logstruct_pattern takes a source and an event and returns the pattern. logstruct_source takes a source and returns it as canonical, with the source's events as a map. Use them to build your own metric filters or subscription filters:

data "logstruct_source" "mailer" {
  source = "mailer"
}

data "logstruct_pattern" "email_delivered" {
  source = data.logstruct_source.mailer.canonical
  event  = "delivered"
}

resource "aws_cloudwatch_log_metric_filter" "email_delivered" {
  name           = "Email Delivered Count"
  log_group_name = var.log_group_name
  pattern        = data.logstruct_pattern.email_delivered.pattern

  metric_transformation {
    name      = "mailer_delivered"
    namespace = var.namespace
    value     = "1"
    unit      = "Count"
  }
}

Match on other fields

CloudWatch's JSON filter patterns can compare numbers, but the provider only builds src and evt patterns. Write a pattern with other fields yourself:

resource "aws_cloudwatch_log_metric_filter" "very_slow_sql" {
  name           = "Very Slow SQL Query Count"
  log_group_name = var.log_group_name
  pattern        = "{ $.src = \"app\" && $.evt = \"database\" && $.duration_ms > 1000 }"

  metric_transformation {
    name      = "sql_very_slow"
    namespace = var.namespace
    value     = "1"
    unit      = "Count"
  }
}

To chart the durations instead of counting the lines, set value to "$.duration_ms".