Skip to content

14 - Observability

The plugin SDK provides built-in observability support through structured logging and metrics. This allows you to instrument your plugins for debugging and monitoring without managing the infrastructure yourself.

Logger

The SDK injects a logger into the context of every CRUD operation. The logger is pre-configured with plugin metadata (namespace, operation, resource type).

Getting the Logger

func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) {
    log := plugin.LoggerFromContext(ctx)

    log.Info("creating file resource", "label", req.Label)
    // ... implementation ...
}

Log Levels

Use the appropriate level for your message:

Level Use When
Debug Verbose details useful during development
Info Normal operations (resource created, updated)
Warn Recoverable issues (retry, fallback used)
Error Failures that affect operation outcome
log.Debug("parsing properties", "raw", string(req.Properties))
log.Info("upload started", "path", props.Path)
log.Warn("retrying after timeout", "attempt", 2)
log.Error("failed to connect", "error", err.Error())

Adding Context with With()

Create a child logger with additional fields that persist across calls:

log := plugin.LoggerFromContext(ctx)
log = log.With("requestID", requestID)

log.Info("operation started")  // includes requestID
log.Debug("step 1 complete")   // includes requestID

Configuring Log Level

Plugin logs are captured by the agent and follow the agent's logging configuration. See the Logging section in the configuration guide to adjust log levels.

Metrics

The SDK also provides a MetricRegistry for recording custom metrics. These are exported via OpenTelemetry when enabled in the agent.

Getting the MetricRegistry

func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) {
    metrics := plugin.MetricsFromContext(ctx)

    // Record a counter
    metrics.Counter("sftp.uploads_started", 1,
        attribute.String("path", props.Path))

    // ... implementation ...
}

Available Metric Types

The SDK exposes four metric types from OpenTelemetry's metric model:

// Counter - monotonically increasing value
metrics.Counter("sftp.uploads_total", 1,
    attribute.String("status", "success"))

// UpDownCounter - value that can increase or decrease
metrics.UpDownCounter("sftp.active_connections", 1)  // connection opened
metrics.UpDownCounter("sftp.active_connections", -1) // connection closed

// Gauge - point-in-time measurement
metrics.Gauge("sftp.queue_depth", float64(len(queue)))

// Histogram - distribution of values (durations, sizes)
start := time.Now()
// ... operation ...
metrics.Histogram("sftp.upload_duration_ms",
    float64(time.Since(start).Milliseconds()))

Complete Example

Here's the SFTP plugin's Create method with observability:

func (p *Plugin) Create(ctx context.Context, req *resource.CreateRequest) (*resource.CreateResult, error) {
    // Get observability from context
    log := plugin.LoggerFromContext(ctx)
    metrics := plugin.MetricsFromContext(ctx)

    log.Info("creating file resource", "label", req.Label)

    // Parse file properties from request
    props, err := parseFileProperties(req.Properties)
    if err != nil {
        log.Error("invalid properties", "error", err.Error())
        return &resource.CreateResult{
            ProgressResult: &resource.ProgressResult{
                Operation:       resource.OperationCreate,
                OperationStatus: resource.OperationStatusFailure,
                ErrorCode:       resource.OperationErrorCodeInvalidRequest,
                StatusMessage:   err.Error(),
            },
        }, nil
    }

    // ... client setup ...

    // Start async upload
    requestID := client.StartUpload(props.Path, props.Content, perm)

    // Record metric for uploads started
    metrics.Counter("sftp.uploads_started", 1,
        attribute.String("path", props.Path))

    log.Debug("upload started", "requestID", requestID, "path", props.Path)

    return &resource.CreateResult{
        ProgressResult: &resource.ProgressResult{
            Operation:       resource.OperationCreate,
            OperationStatus: resource.OperationStatusInProgress,
            RequestID:       requestID,
            NativeID:        props.Path,
        },
    }, nil
}

No-Op Fallback

If no logger or metrics are configured (e.g., during unit tests), LoggerFromContext and MetricsFromContext return no-op implementations that safely ignore all calls. Your code doesn't need nil checks.


Previous: 13 - Local Testing | Next: 15 - Real-World Plugins