Distributed Tracing Example

A complete example showing how to trace a multi-step operation including database queries, external API calls, and correlated logs.

Setup
import { LightningLogsClient } from '@lightning-logs/sdk'

const client = new LightningLogsClient({
  baseURL: process.env.SUPABASE_URL!,
  getAuthToken: async () => process.env.LIGHTNING_LOGS_API_KEY!,
  serviceName: 'api',
  environment: process.env.NODE_ENV ?? 'production',
})
Tracing an HTTP Request Handler
Trace a Next.js API route end-to-end with child spans for each operation
// app/api/orders/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { client } from '@/lib/lightning-logs'

export async function POST(request: NextRequest) {
  const body = await request.json()

  const trace = client.startTrace({
    name: 'api.create-order',
    metadata: {
      route: '/api/orders',
      method: 'POST',
    },
  })

  try {
    // Validate input
    const validateSpan = trace.startSpan('validate-input')
    const { userId, items } = validateOrder(body)
    validateSpan.end({ status: 'ok', metadata: { item_count: items.length } })

    // Check inventory
    const inventorySpan = trace.startSpan('db.check-inventory')
    const available = await checkInventory(items)
    inventorySpan.end({ status: 'ok', metadata: { all_available: available } })

    if (!available) {
      trace.log.warn('Order failed: items out of stock', {
        user_id: userId,
        attrs: { item_count: items.length },
      })
      await trace.end({ status: 'error' })
      return NextResponse.json({ error: 'Out of stock' }, { status: 409 })
    }

    // Create order in DB
    const dbSpan = trace.startSpan('db.create-order')
    const order = await db.transaction(async (trx) => {
      const insertOrderSpan = dbSpan.startSpan('insert-order')
      const o = await trx.insert(orders).values({ userId }).returning()
      insertOrderSpan.end({ status: 'ok' })

      const insertItemsSpan = dbSpan.startSpan('insert-order-items')
      await trx.insert(orderItems).values(items.map(i => ({ orderId: o.id, ...i })))
      insertItemsSpan.end({ status: 'ok', metadata: { count: items.length } })

      return o
    })
    dbSpan.end({ status: 'ok', metadata: { order_id: order.id } })

    // Charge payment
    const paymentSpan = trace.startSpan('stripe.charge')
    const charge = await stripe.paymentIntents.create({
      amount: calculateTotal(items),
      currency: 'usd',
      customer: userId,
    })
    paymentSpan.end({ status: 'ok', metadata: { charge_id: charge.id } })

    trace.log.info('Order created', {
      user_id: userId,
      attrs: {
        order_id: order.id,
        item_count: items.length,
        total_cents: calculateTotal(items),
      },
    })

    await trace.end({ status: 'ok' })
    return NextResponse.json({ orderId: order.id })
  } catch (error: any) {
    trace.log.error('Order creation failed', {
      attrs: { error: error.message },
    })
    await trace.end({ status: 'error' })
    return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
  }
}
Using trace.wrap() for Cleaner Code
The wrap() helper removes boilerplate — spans and trace end automatically
await client.tracer.trace(
  { name: 'process-batch', metadata: { batch_size: jobs.length } },
  async (trace) => {
    for (const job of jobs) {
      await trace.wrap(`process-job.${job.type}`, async (span) => {
        const fetchSpan = span.startSpan('fetch-data')
        const data = await fetchJobData(job.id)
        fetchSpan.end({ status: 'ok' })

        const processSpan = span.startSpan('transform')
        const result = await transform(data)
        processSpan.end({ status: 'ok', metadata: { output_size: result.length } })

        trace.log.info(`Job ${job.id} complete`, {
          attrs: { job_type: job.type, duration_hint: 'see span' }
        })
      })
    }
  }
)
Tracking AI Calls
Mark spans as AI calls to see model usage and cost in the traces view
const trace = client.startTrace({ name: 'ai.summarize-document' })

const extractSpan = trace.startSpan('extract-text')
const text = await extractPdfText(documentUrl)
extractSpan.end({ status: 'ok', metadata: { char_count: text.length } })

const llmSpan = trace.startSpan('openai.summarize', {
  isAI: true,
  model: 'gpt-4o-mini',
})

const response = await openai.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [
    { role: 'system', content: 'Summarize the following document.' },
    { role: 'user', content: text },
  ],
})

llmSpan.end({
  status: 'ok',
  tokens: response.usage?.total_tokens,
  cost: (response.usage?.total_tokens ?? 0) * 0.00000015, // $0.15 / 1M tokens
  metadata: { finish_reason: response.choices[0].finish_reason },
})

trace.log.info('Document summarized', {
  attrs: {
    document_url: documentUrl,
    tokens_used: response.usage?.total_tokens,
  },
})

await trace.end({ status: 'ok' })
Frontend Page Tracing
Trace page navigations in Next.js — automatically correlates page view logs to the trace
'use client'

import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
import { client } from '@/lib/lightning-logs'

export function TraceInit() {
  const pathname = usePathname()

  useEffect(() => {
    const trace = client.startTrace({
      name: 'ui.page_navigation',
      metadata: { path: pathname },
    })

    const renderSpan = trace.startSpan('ui.route_render', {
      metadata: { path: pathname },
    })

    // This log is automatically correlated to the trace ID
    trace.log.info(`Page view: ${pathname}`, {
      type: 'analytics',
      event_type: 'page_view',
      route: pathname,
    })

    renderSpan.end({ status: 'ok' })
    trace.end({ status: 'ok' }).catch(() => {})
  }, [pathname])

  return null
}

Related