Skip to content

Commit 1787df0

Browse files
committed
Expand README and fix CI
1 parent 302738a commit 1787df0

13 files changed

Lines changed: 247 additions & 65 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,23 @@ on:
44
push:
55
branches:
66
- main
7-
87
pull_request:
98

109
jobs:
1110
test:
1211
runs-on: ubuntu-latest
13-
name: Ruby ${{ matrix.ruby }}
14-
strategy:
15-
matrix:
16-
ruby:
17-
- '3.3'
18-
- '3.2'
19-
- '3.1'
20-
2112
steps:
2213
- uses: actions/checkout@v4
2314

24-
- name: Set up Ruby
15+
- name: Set up Ruby (rbenv)
2516
uses: ruby/setup-ruby@v1
2617
with:
27-
ruby-version: ${{ matrix.ruby }}
18+
ruby-version-file: .ruby-version
19+
bundler: '2.6.9'
2820
bundler-cache: true
2921

3022
- name: Run tests
31-
run: bundle exec rspec spec/unit/
23+
run: bundle exec rspec spec/unit
3224

3325
- name: Check gem can be built
3426
run: gem build lf-cli.gemspec

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
# for a library or gem, you might want to ignore these files since the code is
2828
# intended to run in multiple environments; otherwise, check them in:
2929
Gemfile.lock
30-
.ruby-version
3130
.ruby-gemset
3231

3332
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:

.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.4.5

README.md

Lines changed: 188 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
A powerful command-line interface for querying and analyzing Langfuse LLM observability data. Built with Ruby and designed for developers who prefer working in the terminal.
66

7-
[![Gem Version](https://badge.fury.io/rb/lf-cli.svg)](https://badge.fury.io/rb/lf-cli)
7+
[![Gem Version](https://img.shields.io/gem/v/lf-cli)](https://rubygems.org/gems/lf-cli)
8+
[![Total Downloads](https://img.shields.io/gem/dt/lf-cli)](https://rubygems.org/gems/lf-cli)
89
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
910

1011
## Disclaimer
@@ -294,6 +295,192 @@ Configuration is loaded in this order (highest to lowest priority):
294295
3. Config file (`~/.langfuse/config.yml`)
295296
4. Defaults
296297

298+
## LLM Reference (llms-full)
299+
300+
This section mirrors the depth of a `llms-full.txt` so AI assistants can answer detailed questions about `lf-cli` without inspecting the source.
301+
302+
### CLI Metadata
303+
304+
| Item | Details |
305+
| --- | --- |
306+
| Binary names | `lf` (preferred), `langfuse` (legacy alias used in docs) |
307+
| Entry point | `bin/lf` → `Langfuse::CLI::Main.start(ARGV)` |
308+
| Ruby compatibility | Ruby >= 2.7.0 (same as gemspec requirement) |
309+
| Default API host | `https://cloud.langfuse.com` |
310+
| Config path | `~/.langfuse/config.yml` (`0600` permissions) |
311+
| Source namespace | `Langfuse::CLI` with command classes under `lib/langfuse/cli/commands` |
312+
| HTTP stack | Faraday + JSON middleware, 2s open/read timeouts, retry with exponential backoff on `429/5xx` |
313+
| Logging | Set `DEBUG=1` to enable Faraday request/response logging |
314+
315+
### Authentication & Configuration
316+
317+
1. Credentials (`public_key`, `secret_key`) plus `host` are mandatory for all API calls.
318+
2. Resolution order: CLI flags → environment variables → profile in `~/.langfuse/config.yml` → defaults.
319+
3. Profiles let you store multiple environments (e.g., `default`, `staging`, `production`) inside the same YAML file.
320+
4. The `config setup` command validates keys before saving by hitting `/api/public/traces` with `limit=1`.
321+
322+
**Environment variable matrix**
323+
324+
| Variable | Purpose | Required | Notes |
325+
| --- | --- | --- | --- |
326+
| `LANGFUSE_PUBLIC_KEY` | Public API key | ✅ | Required for non-interactive `config setup` and all other commands if no profile is configured. |
327+
| `LANGFUSE_SECRET_KEY` | Secret API key | ✅ | Same priority rules as the public key. |
328+
| `LANGFUSE_HOST` | Override API base URL | Optional | Use for self-hosted Langfuse instances. |
329+
| `LANGFUSE_PROFILE` | Profile name | Optional | Overrides the profile selected via `-P/--profile`. |
330+
| `LANGFUSE_PROJECT_NAME` | Used by `config setup` to show the correct settings URL. | Optional | Only needed for UX hints. |
331+
| `DEBUG` | When set to `1`, logs Faraday requests/responses to stdout. | Optional | Useful for diagnosing API issues. |
332+
333+
### Global Flags & Behavior
334+
335+
| Flag | Description | Notes |
336+
| --- | --- | --- |
337+
| `-P, --profile PROFILE` | Selects a saved profile. | Defaults to `default`. |
338+
| `--public-key KEY` / `--secret-key KEY` | Inject credentials without touching config files. | Highest priority source. |
339+
| `--host URL` | Override Langfuse host. | Combine with `--profile` to temporarily test another region. |
340+
| `-f, --format FORMAT` | `table` (default), `json`, `csv`, `markdown`. | Applies to every command; CSV/Markdown require structured arrays. |
341+
| `-o, --output PATH` | Write output to a file. | Respects format; prints “Output written…” when `--verbose`. |
342+
| `-l, --limit N` | Caps number of records pulled per command. | Pagination helper, defaults to API `limit` (50) when omitted. |
343+
| `-p, --page N` | Start from an explicit page. | Useful when you know an offset. |
344+
| `--from`, `--to` | ISO 8601 or natural language timestamps. | Natural language parsing uses `chronic` if installed; otherwise the string is sent as-is. |
345+
| `-v, --verbose` | Prints extra logs (e.g., file paths). | Some commands emit status lines prefixed with emojis. |
346+
| `--no-color` | Forces monochrome table output. | Forwarded to formatters that support color. |
347+
348+
Pagination strategy: the client keeps fetching pages until it collects the requested `limit` or no more pages remain. `limit` therefore caps the total combined size, not per-page size.
349+
350+
### Output & Files
351+
352+
- `table` renders ASCII tables via `Formatters::TableFormatter`.
353+
- `json` streams `JSON.pretty_generate` for direct piping to `jq`.
354+
- `csv` and `markdown` use dedicated formatters and require array-like data (single hashes are wrapped automatically).
355+
- `--output` writes the formatted string verbatim; combine with `--format json` for scripts.
356+
- Use `lf ... --format json | jq ...` for automation recipes.
357+
358+
### Command Reference
359+
360+
Each command inherits the global flags above. API errors exit with status code `1`.
361+
362+
#### `config` (profile management)
363+
364+
| Subcommand | Synopsis | Notes |
365+
| --- | --- | --- |
366+
| `lf config setup` | Interactive wizard; supports env-variable non-interactive mode. | Tests credentials before saving. |
367+
| `lf config set PROFILE --public-key ... --secret-key ... [--host ...]` | Writes/updates a profile directly. | Does not hit the API. |
368+
| `lf config show [PROFILE]` | Prints the resolved profile (keys masked). | Reads from YAML + ENV. |
369+
| `lf config list` | Shows every profile name plus masked public key/host. | Warns if file missing. |
370+
371+
#### `traces`
372+
373+
| Subcommand | Purpose |
374+
| --- | --- |
375+
| `lf traces list` | Lists traces with filters/pagination. |
376+
| `lf traces get TRACE_ID [--with-observations]` | Fetches a single trace. The `--with-observations` flag is accepted for forward compatibility but currently behaves the same as the default API payload. |
377+
378+
`traces list` options:
379+
380+
| Flag | Type | Description |
381+
| --- | --- | --- |
382+
| `--name NAME` | String | Filter by trace name. |
383+
| `--user-id USER_ID` | String | Filter by Langfuse user identifier. |
384+
| `--session-id SESSION_ID` | String | Filter by session. |
385+
| `--tags TAG1 TAG2` | Array | Matches traces containing all provided tags. |
386+
| `--from`, `--to` | String | Time boundaries; accepts ISO 8601 or relative strings. |
387+
| `--limit`, `--page` | Numeric | Override pagination per request. |
388+
389+
Sample workflow:
390+
391+
```bash
392+
latest_trace_id=$(lf traces list --format json --limit 1 | jq -r '.[0].id')
393+
lf traces get "$latest_trace_id" --format json > trace.json
394+
```
395+
396+
#### `sessions`
397+
398+
| Subcommand | Purpose |
399+
| --- | --- |
400+
| `lf sessions list` | Enumerates sessions. |
401+
| `lf sessions show SESSION_ID [--with-traces]` | Shows a session and optionally its traces (flag reserved for future enrichments). |
402+
403+
Options mirror trace pagination: `--from`, `--to`, `--limit`, `--page`.
404+
405+
#### `observations`
406+
407+
| Subcommand | Purpose |
408+
| --- | --- |
409+
| `lf observations list` | Lists generations, spans, or events. |
410+
| `lf observations get OBSERVATION_ID` | Fetches a single observation. |
411+
412+
`list` filters:
413+
414+
| Flag | Values | Description |
415+
| --- | --- | --- |
416+
| `--type` | `generation`, `span`, `event` | Restrict to an observation type. |
417+
| `--trace-id` | Trace ID | Only observations under a specific trace. |
418+
| `--name` | String | Filter by observation name. |
419+
| `--user-id` | String | Filter by associated user. |
420+
| `--from`, `--to`, `--limit`, `--page` | As described earlier. |
421+
422+
#### `scores`
423+
424+
| Subcommand | Purpose |
425+
| --- | --- |
426+
| `lf scores list` | Lists evaluation scores. |
427+
| `lf scores get SCORE_ID` | Fetches a single score document. |
428+
429+
Filters: `--name`, `--from`, `--to`, `--limit`, `--page`.
430+
431+
#### `metrics`
432+
433+
Single subcommand: `lf metrics query`.
434+
435+
Required flags:
436+
437+
| Flag | Allowed values | Description |
438+
| --- | --- | --- |
439+
| `--view` | `traces`, `observations`, `scores-numeric`, `scores-categorical` | Which metrics view to query. |
440+
| `--measure` | `count`, `latency`, `value`, `tokens`, `cost` | Base metric. |
441+
| `--aggregation` | `count`, `sum`, `avg`, `p50`, `p95`, `p99`, `min`, `max`, `histogram` | Aggregation function. |
442+
443+
Optional flags:
444+
445+
| Flag | Description |
446+
| --- | --- |
447+
| `--dimensions field1 field2` | Array of dimension field names (e.g., `name`, `userId`, `sessionId`, `model`). |
448+
| `--from`, `--to` | Time range. |
449+
| `--granularity` | `minute`, `hour`, `day`, `week`, `month`, `auto`. Controls the time bucket. |
450+
| `--limit` | Defaults to `100` for metrics; caps the number of buckets/rows returned. |
451+
452+
The CLI builds a payload matching the Langfuse metrics API (`metrics` array, `timeDimension` etc.) via `Langfuse::CLI::Types::MetricsQuery`.
453+
454+
### Data Model Cheat Sheet
455+
456+
- **Trace**: `id`, `name`, `userId`, `sessionId`, `timestamp`, `durationMs`, `tags[]`, `metadata` (object), `observations[]` (optional when using `get`), `scores[]`.
457+
- **Session**: `id`, `userId`, `name`, `createdAt`, `updatedAt`, `traceIds[]`, `metadata`.
458+
- **Observation**: `id`, `traceId`, `type` (`generation/span/event`), `name`, `status`, `model`, `input`, `output`, `metrics` (latency, usage), `level`, `parentObservationId`.
459+
- **Score**: `id`, `name`, `value` (number/string), `type` (`numeric`/`categorical`), `traceId`, `observationId`, `timestamp`, `metadata`, `comment`.
460+
- **Metrics response**: Usually `{ "data": [ { "dimensions": {...}, "metrics": {...} } ], "meta": {...} }`. The CLI automatically unwraps `data` before formatting.
461+
462+
Fields are passed through verbatim from the Langfuse Public API; the CLI never renames keys.
463+
464+
### Error Handling & Exit Codes
465+
466+
- Success exits with code `0`.
467+
- Any `Langfuse::CLI::Client::*Error` results in exit code `1` after printing a human-readable message.
468+
- Specific messages:
469+
- `Authentication Error` for `401`.
470+
- `Rate limit exceeded` for `429`.
471+
- `Trace/session/... not found` for `404`.
472+
- `Request timed out` when Faraday raises a timeout (usually after 2s).
473+
- Use `--verbose` or `DEBUG=1` for deeper context (e.g., stack traces, Faraday logs).
474+
475+
### Troubleshooting & Automation Tips
476+
477+
- **Network issues**: verify `LANGFUSE_HOST` by running `lf config show` and hitting `/health` with `curl`.
478+
- **Time parsing**: install the `chronic` gem to enable natural language ranges; otherwise pass ISO 8601 timestamps.
479+
- **CSV exports**: always provide `--output file.csv` to avoid large terminal dumps.
480+
- **Scripting**: prefer `--format json` to keep machine-readable structures. Most commands return arrays, so piping to `jq '.[].id'` works consistently.
481+
- **Profiles**: store CI credentials under `LANGFUSE_PROFILE=ci` and load them via `lf ... -P ci` to keep human/dev credentials untouched.
482+
- **Retries**: built-in Faraday retry middleware already backs off (`max: 3`). For long-running scripts, wrap commands with shell retries instead of adding loops inside the CLI.
483+
297484
## Development
298485

299486
### Setup

lib/langfuse/cli/commands/config.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ def setup
8888
rescue Client::TimeoutError => e
8989
prompt.error("Connection test failed: #{e.message}")
9090
prompt.error("The host '#{host}' may be incorrect or unreachable.")
91-
exit 1
91+
raise_cli_error("Connection test failed: #{e.message}")
9292
rescue Client::AuthenticationError => e
9393
prompt.error("Connection test failed: #{e.message}")
9494
prompt.error("Please check your credentials and try again.")
95-
exit 1
95+
raise_cli_error("Connection test failed: #{e.message}")
9696
rescue Client::APIError => e
9797
prompt.error("Connection test failed: #{e.message}")
98-
exit 1
98+
raise_cli_error("Connection test failed: #{e.message}")
9999
end
100100
end
101101

@@ -170,6 +170,10 @@ def mask_key(key)
170170

171171
"#{key[0..7]}#{'*' * (key.length - 8)}"
172172
end
173+
174+
def raise_cli_error(message)
175+
raise Langfuse::CLI::Error, message
176+
end
173177
end
174178
end
175179
end

lib/langfuse/cli/commands/metrics.rb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,9 @@ def query
7979
output_data = result.is_a?(Hash) && result['data'] ? result['data'] : result
8080
output_result(output_data)
8181
rescue Client::AuthenticationError => e
82-
puts "Authentication Error: #{e.message}"
83-
exit 1
82+
raise_cli_error("Authentication Error: #{e.message}")
8483
rescue Client::APIError => e
85-
puts "Error: #{e.message}"
86-
exit 1
84+
raise_cli_error("Error: #{e.message}")
8785
end
8886

8987
private
@@ -187,6 +185,10 @@ def parent_options
187185
{}
188186
end
189187
end
188+
189+
def raise_cli_error(message)
190+
raise Langfuse::CLI::Error, message
191+
end
190192
end
191193
end
192194
end

lib/langfuse/cli/commands/observations.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,19 @@ def list
5757
observations = client.list_observations(filters)
5858
output_result(observations)
5959
rescue Client::AuthenticationError => e
60-
puts "Authentication Error: #{e.message}"
61-
exit 1
60+
raise_cli_error("Authentication Error: #{e.message}")
6261
rescue Client::APIError => e
63-
puts "Error: #{e.message}"
64-
exit 1
62+
raise_cli_error("Error: #{e.message}")
6563
end
6664

6765
desc 'get OBSERVATION_ID', 'Get a specific observation'
6866
def get(observation_id)
6967
observation = client.get_observation(observation_id)
7068
output_result(observation)
71-
rescue Client::NotFoundError => e
72-
puts "Error: Observation not found - #{observation_id}"
73-
exit 1
69+
rescue Client::NotFoundError
70+
raise_cli_error("Observation not found - #{observation_id}")
7471
rescue Client::APIError => e
75-
puts "Error: #{e.message}"
76-
exit 1
72+
raise_cli_error("Error: #{e.message}")
7773
end
7874

7975
private
@@ -155,6 +151,10 @@ def parent_options
155151
{}
156152
end
157153
end
154+
155+
def raise_cli_error(message)
156+
raise Langfuse::CLI::Error, message
157+
end
158158
end
159159
end
160160
end

lib/langfuse/cli/commands/scores.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,19 @@ def list
2121
scores = client.list_scores(filters)
2222
output_result(scores)
2323
rescue Client::AuthenticationError => e
24-
puts "Authentication Error: #{e.message}"
25-
exit 1
24+
raise_cli_error("Authentication Error: #{e.message}")
2625
rescue Client::APIError => e
27-
puts "Error: #{e.message}"
28-
exit 1
26+
raise_cli_error("Error: #{e.message}")
2927
end
3028

3129
desc 'get SCORE_ID', 'Get a specific score'
3230
def get(score_id)
3331
score = client.get_score(score_id)
3432
output_result(score)
35-
rescue Client::NotFoundError => e
36-
puts "Error: Score not found - #{score_id}"
37-
exit 1
33+
rescue Client::NotFoundError
34+
raise_cli_error("Score not found - #{score_id}")
3835
rescue Client::APIError => e
39-
puts "Error: #{e.message}"
40-
exit 1
36+
raise_cli_error("Error: #{e.message}")
4137
end
4238

4339
private
@@ -116,6 +112,10 @@ def parent_options
116112
{}
117113
end
118114
end
115+
116+
def raise_cli_error(message)
117+
raise Langfuse::CLI::Error, message
118+
end
119119
end
120120
end
121121
end

0 commit comments

Comments
 (0)