Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
OPENROUTER_API_KEY=your_openrouter_api_key_here

# Langfuse tracing (optional, for LLM observability)
LANGFUSE_HOST=http://localhost:3000
LANGFUSE_PUBLIC_KEY=pk-lf-lapp-dev
LANGFUSE_SECRET_KEY=sk-lf-lapp-dev

# OpenTelemetry tracing (optional, for distributed tracing with Jaeger)
OTEL_TRACING_ENABLED=true
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ go test -v -run TestFunctionName ./pkg/pattern/
## CLI Usage

```bash
go run ./cmd/lapp/ ingest <logfile> [--db <path>] [--model <model>]
go run ./cmd/lapp/ templates [--db <path>]
go run ./cmd/lapp/ analyze <logfile> [question]
go run ./cmd/lapp/ analyze <logfile> [question] [--model <model>] [--db <path>]
go run ./cmd/lapp/ debug workspace <logfile> [-o <dir>]
go run ./cmd/lapp/ debug ingest <logfile> [--model <model>] [--db <path>]
go run ./cmd/lapp/ debug run <workspace-dir> [question] [--model <model>]
```

Expand Down Expand Up @@ -79,7 +78,7 @@ Builds a workspace directory with pre-processed files, then runs an eino ADK age

## Environment Variables

- `OPENROUTER_API_KEY`: Required for `ingest`, `analyze`, and `debug run`
- `OPENROUTER_API_KEY`: Required for `analyze`, `debug ingest`, and `debug run`
- `MODEL_NAME`: Override default LLM model (default: `google/gemini-3-flash-preview`)
- `.env` file is auto-loaded via godotenv

Expand All @@ -91,3 +90,4 @@ Builds a workspace directory with pre-processed files, then runs an eino ADK age

- `nolint` directives go on the line above the target, not as end-of-line comments
- Compile-time interface guards: `var _ MyInterface = (*MyImpl)(nil)`
- Always use `make build` to verify compilation, never bare `go build` (it drops a binary in the project root)
67 changes: 63 additions & 4 deletions cmd/lapp/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import (
"github.com/spf13/cobra"
"github.com/strrl/lapp/pkg/analyzer"
"github.com/strrl/lapp/pkg/multiline"
"github.com/strrl/lapp/pkg/pattern"
"github.com/strrl/lapp/pkg/semantic"
"github.com/strrl/lapp/pkg/store"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)

var analyzeModel string
Expand All @@ -18,8 +24,9 @@ func analyzeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "analyze <logfile> [question]",
Short: "Analyze a log file using an AI agent to find root causes",
Long: `Read a log file, parse it through the template pipeline, then use an AI agent
to autonomously explore the processed logs and provide analysis.
Long: `Read a log file, run the full ingest pipeline (Drain clustering, semantic labeling,
DuckDB storage), then use an AI agent to autonomously explore the processed logs
and provide analysis.

Requires OPENROUTER_API_KEY environment variable to be set.

Expand All @@ -46,6 +53,11 @@ func runAnalyze(cmd *cobra.Command, args []string) error {
question = args[1]
}

ctx, span := otel.Tracer("lapp/cmd").Start(cmd.Context(), "cmd.Analyze")
defer span.End()

span.SetAttributes(attribute.String("log.file", logFile))

// Read all lines
slog.Info("Reading logs...")
lines, err := readLines(logFile)
Expand All @@ -56,20 +68,67 @@ func runAnalyze(cmd *cobra.Command, args []string) error {
if err != nil {
return errors.Errorf("multiline detector: %w", err)
}
merged := multiline.MergeSlice(lines, detector)
merged := multiline.MergeSlice(ctx, lines, detector)
mergedLines := make([]string, len(merged))
for i, m := range merged {
mergedLines[i] = m.Content
}
slog.Info("Read lines", "lines", len(lines), "merged_entries", len(mergedLines))

// Refuse to reuse an existing database to avoid silently mixing datasets
if _, err := os.Stat(dbPath); err == nil {
return errors.Errorf("database %q already exists; remove it first or choose a different --db path", dbPath)
Comment on lines +79 to +80

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove partially created DB on ingest failure

This existence guard makes retries fail after transient errors: runAnalyze creates and initializes dbPath before the LLM labeling/storage steps, but if discoverAndSavePatterns or later ingest work returns an error (for example, temporary OpenRouter/API failures), the command exits without deleting the new file; the next invocation with the same --db immediately aborts here with "database already exists." That turns a recoverable failure into manual cleanup work for every retry.

Useful? React with 👍 / 👎.

}

// Ingest pipeline: Drain clustering + semantic labeling + DuckDB storage
drainParser, err := pattern.NewDrainParser()
if err != nil {
return errors.Errorf("drain parser: %w", err)
}

s, err := store.NewDuckDBStore(dbPath)
if err != nil {
return errors.Errorf("store: %w", err)
}
defer func() { _ = s.Close() }()

if err := s.Init(ctx); err != nil {
return errors.Errorf("store init: %w", err)
}

semanticIDMap, patternCount, templateCount, err := discoverAndSavePatterns(ctx, s, drainParser, mergedLines, semantic.Config{
Comment on lines +95 to +99

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid appending analyze runs into the shared DB

analyze now always opens --db and inserts patterns/log entries, but this flow never scopes data to a run or clears prior rows before writing. Because the schema/query APIs have no source-file/run discriminator, rerunning lapp analyze (especially on different files) silently mixes datasets and inflates counts, which corrupts later PatternSummaries/query results for that DB path. This command previously had no persistent side effects, so the new default behavior is a data-integrity regression for repeated analyses.

Useful? React with 👍 / 👎.

APIKey: apiKey,
Model: analyzeModel,
})
if err != nil {
return err
}

templates, err := drainParser.Templates(ctx)
if err != nil {
return errors.Errorf("drain templates: %w", err)
}
if err := storeLogsWithLabels(ctx, s, merged, templates, semanticIDMap); err != nil {
return err
}

slog.Info("Ingestion complete",
"lines", len(mergedLines),
"templates", templateCount,
"patterns_with_2+_matches", patternCount,
)
slog.Info("Database stored", "path", dbPath)

// Run AI agent analysis
config := analyzer.Config{
APIKey: apiKey,
Model: analyzeModel,
}

result, err := analyzer.Analyze(cmd.Context(), config, mergedLines, question)
result, err := analyzer.AnalyzeWithTemplates(ctx, config, mergedLines, templates, question)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}

Expand Down
26 changes: 22 additions & 4 deletions cmd/lapp/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import (
"github.com/strrl/lapp/pkg/analyzer/workspace"
"github.com/strrl/lapp/pkg/multiline"
"github.com/strrl/lapp/pkg/pattern"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)

func debugCmd() *cobra.Command {
Expand All @@ -20,6 +23,7 @@ func debugCmd() *cobra.Command {

cmd.AddCommand(debugWorkspaceCmd())
cmd.AddCommand(debugRunCmd())
cmd.AddCommand(debugIngestCmd())
return cmd
}

Expand All @@ -41,6 +45,11 @@ files (raw.log, summary.txt, errors.txt) in a local directory for inspection.`,
func runDebugWorkspace(cmd *cobra.Command, args []string) error {
logFile := args[0]

ctx, span := otel.Tracer("lapp/cmd").Start(cmd.Context(), "cmd.DebugWorkspace")
defer span.End()

span.SetAttributes(attribute.String("log.file", logFile))

slog.Info("Reading logs...")
lines, err := readLines(logFile)
if err != nil {
Expand All @@ -50,7 +59,7 @@ func runDebugWorkspace(cmd *cobra.Command, args []string) error {
if err != nil {
return errors.Errorf("multiline detector: %w", err)
}
merged := multiline.MergeSlice(lines, detector)
merged := multiline.MergeSlice(ctx, lines, detector)
mergedLines := make([]string, len(merged))
for i, m := range merged {
mergedLines[i] = m.Content
Expand All @@ -75,10 +84,10 @@ func runDebugWorkspace(cmd *cobra.Command, args []string) error {
}

slog.Info("Parsing entries", "count", len(mergedLines))
if err := drainParser.Feed(mergedLines); err != nil {
if err := drainParser.Feed(ctx, mergedLines); err != nil {
return errors.Errorf("drain feed: %w", err)
}
templates, err := drainParser.Templates()
templates, err := drainParser.Templates(ctx)
if err != nil {
return errors.Errorf("drain templates: %w", err)
}
Expand All @@ -88,6 +97,8 @@ func runDebugWorkspace(cmd *cobra.Command, args []string) error {
}

slog.Info("Workspace created", "dir", outDir)

span.SetStatus(codes.Ok, "")
return nil
}

Expand All @@ -108,6 +119,9 @@ Requires OPENROUTER_API_KEY environment variable to be set.`,
}

func runDebugRun(cmd *cobra.Command, args []string) error {
ctx, span := otel.Tracer("lapp/cmd").Start(cmd.Context(), "cmd.DebugRun")
defer span.End()

apiKey := os.Getenv("OPENROUTER_API_KEY")
if apiKey == "" {
return errors.Errorf("OPENROUTER_API_KEY environment variable is required")
Expand All @@ -129,12 +143,16 @@ func runDebugRun(cmd *cobra.Command, args []string) error {
Model: debugRunModel,
}

span.SetAttributes(attribute.String("workspace.dir", workDir))

slog.Info("Running agent on workspace", "dir", workDir)
result, err := analyzer.RunAgent(cmd.Context(), config, workDir, question)
result, err := analyzer.RunAgent(ctx, config, workDir, question)
if err != nil {
return err
}

slog.Info(result)

span.SetStatus(codes.Ok, "")
return nil
}
Loading
Loading