terrably
Reference

Structured logging

Emit structured JSON logs to stderr in the go-hclog format that Terraform parses and re-emits through its own log pipeline.

Why stderr?

stdout is reserved for the go-plugin handshake protocol. Any non-handshake bytes on stdout will corrupt the gRPC connection. Always write logs to stderr.

terrably uses pino to emit newline-delimited JSON in the go-hclog format –

{"@level":"debug","@timestamp":"2026-04-22T10:30:00.123000Z","@module":"provider","@message":"provider configured","endpoint":"https://api.example.com"}

createLogger(module?)

import { createLogger } from "terrably";
import type { Logger } from "terrably";

createLogger(module?: string): Logger

Prop

Type


Logger interface

interface Logger {
  trace(msg: string, fields?: Record<string, unknown>): void;
  debug(msg: string, fields?: Record<string, unknown>): void;
  info (msg: string, fields?: Record<string, unknown>): void;
  warn (msg: string, fields?: Record<string, unknown>): void;
  error(msg: string, fields?: Record<string, unknown>): void;
  child(fields: Record<string, unknown>): Logger;
}

Extra fields are merged at the root of the JSON object alongside the @-prefixed fields.


Log levels

Prop

Type


Environment variables

Log level is resolved from the environment when createLogger() is first called. If neither variable is set, the logger is silent — no output when the provider runs outside of Terraform.

VariableWins overDescription
TF_LOGeverythingGlobal override — applies to Terraform core and providers
TF_LOG_PROVIDER(default)Provider-specific level — does not affect Terraform core logs

Valid values (case-insensitive): TRACE · DEBUG · INFO · WARN · ERROR · OFF

TF_LOG=JSON is treated as TRACE (the SDK always emits JSON).


Usage

Basic

import { createLogger } from "terrably";

const log = createLogger("provider");

export class MyProvider implements Provider {
  configure(_diags, config) {
    log.info("provider configured", { endpoint: config["api_url"] });
  }
}

Child loggers / subsystems

Use logger.child() to create a logger that inherits level and config but merges additional persistent fields into every line:

// Create a named subsystem logger
const clientLog = log.child({ "@module": "provider.http_client" });
clientLog.debug("sending request", { url: "https://api.example.com/servers", method: "POST" });
// → {"@level":"debug","@module":"provider.http_client","@message":"sending request","url":"...","method":"POST"}

Attach request-scoped context:

export class ServerResource implements Resource {
  private log = createLogger("provider.server");

  async create(ctx: CreateContext, config: State) {
    const reqLog = this.log.child({ resource: "server", name: config["name"] });
    reqLog.debug("creating resource");
    const server = await apiClient.createServer(config);
    reqLog.info("resource created", { id: server.id });
  }
}

Accessing logs via Terraform

# Provider-only logs, suppress core noise
TF_LOG_PROVIDER=DEBUG terraform apply

# All levels, write to file
TF_LOG=DEBUG TF_LOG_PATH=./tf.log terraform apply

# Filter provider lines from mixed output
TF_LOG=TRACE terraform apply 2>&1 | grep '"@module":"provider'

Last updated on

On this page