Skip to content

feat: add langfuse plugin for LLM observability#13095

Open
sihyeonn wants to merge 7 commits intoapache:masterfrom
sihyeonn:feat/langfuse-plugin
Open

feat: add langfuse plugin for LLM observability#13095
sihyeonn wants to merge 7 commits intoapache:masterfrom
sihyeonn:feat/langfuse-plugin

Conversation

@sihyeonn
Copy link
Copy Markdown
Contributor

@sihyeonn sihyeonn commented Mar 18, 2026

Description

Add a new langfuse plugin that sends LLM request traces to Langfuse for AI observability. The plugin uses the batch-processor-manager pattern (same as http-logger) to send trace data via Langfuse's ingestion API.

Key features:

  • Auto-detect AI endpoints via configurable URI suffix patterns
  • Generate trace-create and generation-create batch items per request
  • W3C traceparent header parsing and propagation for distributed tracing
  • Token usage extraction with priority: ctx.ai_token_usage (ai-proxy) > nginx vars > response body parsing
  • Support X-Langfuse-Tags and X-Langfuse-Metadata custom headers
  • encrypt_fields for secret key protection in etcd
  • Connection keepalive for Langfuse API calls

Which issue(s) this PR fixes:

Fixes #

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible (If not, please discuss on the APISIX mailing list first)

@dosubot dosubot bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request plugin labels Mar 18, 2026
@Baoyuantop
Copy link
Copy Markdown
Contributor

This PR contains three fundamentally different features:

  1. Add a Langfuse logging plugin (apisix/plugins/langfuse.lua, line 687)

  2. Consumer label encryption/decryption (apisix/admin/consumers.lua + apisix/consumer.lua)

  3. ai-proxy consumer_label authentication source (apisix/plugins/ai-proxy/base.lua + schema.lua)

Apache APISIX community practice requires each PR to focus on a single feature. These three features have different review dimensions, testing requirements, and regression risks; please separate them into independent PRs.

@Baoyuantop
Copy link
Copy Markdown
Contributor

Hi @sihyeonn, following up on the previous review comments. Please let us know if you have any updates. Thank you.

@sihyeonn sihyeonn force-pushed the feat/langfuse-plugin branch from 187a719 to d3003c0 Compare April 2, 2026 23:48
@Baoyuantop
Copy link
Copy Markdown
Contributor

Hi @sihyeonn, my suggestion is to only keep the feature-related code in the current PR, and submit other changes in separate PRs.

Add a new langfuse plugin that sends AI request traces to Langfuse
via its ingestion API using batch-processor-manager pattern.

Features:
- Auto-detect AI endpoints (configurable patterns)
- Generate trace-create and generation-create batch items per request
- W3C traceparent header parsing and propagation
- Token usage extraction (ai-proxy ctx > nginx vars > response body)
- Support for X-Langfuse-Tags and X-Langfuse-Metadata headers
- Basic auth with encrypt_fields for secret key protection
- Connection keepalive for Langfuse API calls

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
Move langfuse_host, langfuse_public_key, langfuse_secret_key, ssl_verify,
timeout, detect_ai_requests, and ai_endpoints from per-route schema to
metadata_schema (plugin_attr), following the opentelemetry plugin pattern.

Per-route schema now only contains include_metadata.
Remove encrypt_fields (custom feature, not in OSS).

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
Remove /v1-prefixed entries since has_suffix matching already covers
them via shorter suffixes (e.g. /chat/completions matches both
/chat/completions and /v1/chat/completions).

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
@sihyeonn sihyeonn force-pushed the feat/langfuse-plugin branch from d3003c0 to 9c4b25a Compare April 9, 2026 23:08
Copy link
Copy Markdown
Contributor

@Baoyuantop Baoyuantop left a comment

Choose a reason for hiding this comment

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

Please add plugin doc

Comment thread apisix/plugins/langfuse.lua Outdated

local _M = {
version = 0.1,
priority = 398,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

clickhouse-logger has same priority with langfuse

Comment thread apisix/plugins/langfuse.lua Outdated
if not trace_id then
local request_id = core.request.header(ctx, "X-Request-Id")
if request_id then
trace_id = request_id
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

According to the W3C traceparent specification, the trace_id must consist of 32 hex characters. The X-Request-Id is typically in UUID format (with hyphens, 36 characters); using it directly as the trace_id will result in a traceparent header that does not comply with the specification. Recommendation: Either remove the hyphens during conversion, or do not use the X-Request-Id as the trace_id.

Add English documentation for the langfuse plugin covering:
- Plugin description and purpose
- Plugin metadata attributes (global config via plugin_attr)
- Per-route schema attributes
- Enable/disable instructions with examples
- Register in docs config.json under Loggers

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
- Change priority from 398 to 396 to avoid conflict with clickhouse-logger
- Strip hyphens from X-Request-Id and UUID-generated IDs when used as
  trace_id, ensuring compliance with W3C traceparent spec which requires
  trace_id to be exactly 32 lowercase hex characters
- Validate that the resulting trace_id matches the expected format,
  falling back to a freshly generated UUID if it does not

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
@sihyeonn sihyeonn requested a review from Baoyuantop April 16, 2026 22:46
content_by_lua blocks do not inherit globals — add local core require
in TEST 3 and TEST 4 to fix 'attempt to index global core (a nil value)'.

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
local plugin_name = "langfuse"
local batch_processor_manager = bp_manager_mod.new("langfuse logger")

local metadata_schema = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing encrypt_fields: langfuse_secret_key in plugin_metadata will be stored in plaintext in etcd. Please either add encrypt_fields support or document this risk clearly. See lago.lua L111 for reference.

Comment thread apisix/plugins/langfuse.lua Outdated
if schema_type == core.schema.TYPE_METADATA then
return core.schema.check(metadata_schema, conf)
end
return core.schema.check(schema, conf)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

check_schema uses unwrapped schema: _M.schema is wrapped via batch_processor_manager:wrap_schema(schema), but check_schema validates against the raw schema for non-metadata types. This means batch processor config fields (e.g., batch_max_size) will fail validation. Please change to:

return core.schema.check(_M.schema, conf)

Comment thread conf/config.yaml.example Outdated
- udp-logger # priority: 400
- file-logger # priority: 399
- clickhouse-logger # priority: 398
- langfuse # priority: 396
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

langfuse (priority 396) should be listed after tencent-cloud-cls (priority 397) to maintain descending priority order

- Add encrypt_fields for langfuse_secret_key in metadata_schema to
  prevent plaintext storage in etcd
- Fix check_schema to validate against _M.schema (batch-processor-wrapped)
  instead of raw schema, so batch_max_size and other batch fields pass
  validation
- Fix plugin list order in config.yaml.example to maintain descending
  priority order (tencent-cloud-cls:397 before langfuse:396)

Signed-off-by: Sihyeon Jang <sihyeon.jang@navercorp.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request plugin size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants