Skip to content

Commit 149dcf6

Browse files
sallyomclaude
andauthored
update langfuse deployment, configure logging (#463)
The helm values file to configure log retention isn't working. This PR adds a post-install script to configure langfuse logging to avoid langfuse-clickhouse PVC from filling with logs we don't need or want to retain. --------- Signed-off-by: sallyom <somalley@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent fc75f47 commit 149dcf6

File tree

9 files changed

+837
-5
lines changed

9 files changed

+837
-5
lines changed

.github/workflows/runner-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ jobs:
3838
run: |
3939
# Only run standalone unit tests that don't require runner_shell runtime
4040
# (test_model_mapping.py and test_wrapper_vertex.py require full runtime environment)
41-
pytest tests/test_observability.py tests/test_security_utils.py -v --tb=short --color=yes
41+
pytest tests/test_observability.py tests/test_security_utils.py tests/test_privacy_masking.py -v --tb=short --color=yes
4242
4343
- name: Run tests with coverage
4444
run: |
45-
pytest tests/test_observability.py tests/test_security_utils.py --cov=observability --cov=security_utils --cov-report=term-missing --cov-report=xml
45+
pytest tests/test_observability.py tests/test_security_utils.py tests/test_privacy_masking.py --cov=observability --cov=security_utils --cov-report=term-missing --cov-report=xml
4646
4747
- name: Upload coverage to Codecov
4848
uses: codecov/codecov-action@v4

CLAUDE.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,86 @@ The Claude Code runner (`components/runners/claude-code-runner/`) provides:
335335
- **API version**: `v1alpha1` (current)
336336
- **RBAC**: Namespace-scoped service accounts with minimal permissions
337337

338+
### Langfuse Observability (LLM Tracing)
339+
340+
The platform includes optional Langfuse integration for LLM observability, tracking usage metrics while protecting user privacy.
341+
342+
#### Privacy-First Design
343+
344+
- **Default behavior**: User messages and assistant responses are **REDACTED** in traces
345+
- **Preserved data**: Usage metrics (tokens, costs), metadata (model, turn count, timestamps)
346+
- **Rationale**: Track costs and usage patterns without exposing potentially sensitive user data
347+
348+
#### Configuration
349+
350+
**Enable Langfuse** (disabled by default):
351+
```bash
352+
# In ambient-admin-langfuse-secret
353+
LANGFUSE_ENABLED=true
354+
LANGFUSE_PUBLIC_KEY=<your-key>
355+
LANGFUSE_SECRET_KEY=<your-secret>
356+
LANGFUSE_HOST=http://langfuse-web.langfuse.svc.cluster.local:3000
357+
```
358+
359+
**Privacy Controls** (optional - masking enabled by default):
360+
```bash
361+
# Masking is ENABLED BY DEFAULT (no environment variable needed)
362+
# The runner defaults to LANGFUSE_MASK_MESSAGES=true if not set
363+
364+
# To explicitly set (optional):
365+
LANGFUSE_MASK_MESSAGES=true
366+
367+
# To disable masking (dev/testing ONLY - exposes full message content):
368+
LANGFUSE_MASK_MESSAGES=false
369+
```
370+
371+
#### Deployment
372+
373+
Deploy Langfuse to your cluster:
374+
```bash
375+
# Deploy with default privacy-preserving settings
376+
./e2e/scripts/deploy-langfuse.sh
377+
378+
# For OpenShift
379+
./e2e/scripts/deploy-langfuse.sh --openshift
380+
381+
# For Kubernetes
382+
./e2e/scripts/deploy-langfuse.sh --kubernetes
383+
```
384+
385+
#### Implementation
386+
387+
- **Location**: `components/runners/claude-code-runner/observability.py`
388+
- **Masking function**: `_privacy_masking_function()` - redacts content while preserving metrics
389+
- **Test coverage**: `tests/test_privacy_masking.py` - validates masking behavior
390+
391+
#### What Gets Logged
392+
393+
**With Masking Enabled (Default)**:
394+
- ✅ Token counts (input, output, cache read, cache creation)
395+
- ✅ Cost calculations (USD per session)
396+
- ✅ Model names and versions
397+
- ✅ Turn counts and session durations
398+
- ✅ Tool usage (names, execution status)
399+
- ✅ Error states and completion status
400+
- ❌ User prompts (redacted)
401+
- ❌ Assistant responses (redacted)
402+
- ❌ Tool outputs with long content (redacted)
403+
404+
**With Masking Disabled** (dev/testing only):
405+
- ✅ All of the above
406+
- ⚠️ Full user message content (potentially sensitive!)
407+
- ⚠️ Full assistant response content
408+
- ⚠️ Complete tool outputs
409+
410+
#### OpenTelemetry Support
411+
412+
Langfuse supports OpenTelemetry as of 2025:
413+
- **Current implementation**: Langfuse Python SDK (v3, OTel-based)
414+
- **Alternative**: Pure OpenTelemetry SDK → Langfuse OTLP endpoint (`/api/public/otel`)
415+
- **Migration**: Not recommended unless vendor neutrality is required
416+
- **Benefit**: Current SDK already uses OTel underneath
417+
338418
## Backend and Operator Development Standards
339419

340420
**IMPORTANT**: When working on backend (`components/backend/`) or operator (`components/operator/`) code, you MUST follow these strict guidelines based on established patterns in the codebase.

components/manifests/base/ambient-admin-langfuse-secret.yaml.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# --from-literal=LANGFUSE_SECRET_KEY=sk-lf-YOUR-SECRET-KEY-HERE \
1717
# --from-literal=LANGFUSE_HOST=http://langfuse-web.langfuse.svc.cluster.local:3000 \
1818
# --from-literal=LANGFUSE_ENABLED=true \
19+
# --from-literal=LANGFUSE_MASK_MESSAGES=true \
1920
# -n ambient-code
2021
#
2122
# Option 2: Using this YAML file (less secure - keys visible in manifest):
@@ -43,3 +44,19 @@ stringData:
4344

4445
# Enable Langfuse observability for all sessions
4546
LANGFUSE_ENABLED: "true"
47+
48+
# Privacy Controls: Mask user messages and assistant responses in traces
49+
# Default: "true" (privacy-first - redacts message content, preserves usage metrics)
50+
# Set to "false" only for dev/testing environments where full message logging is needed
51+
#
52+
# What gets logged with LANGFUSE_MASK_MESSAGES=true (recommended for production):
53+
# ✅ Token counts (input, output, cache read/creation)
54+
# ✅ Cost calculations (USD per session)
55+
# ✅ Model names, turn counts, session metadata
56+
# ✅ Tool names and execution status
57+
# ❌ User prompts → [REDACTED FOR PRIVACY]
58+
# ❌ Assistant responses → [REDACTED FOR PRIVACY]
59+
# ❌ Long tool outputs → [REDACTED FOR PRIVACY]
60+
#
61+
# NOTE: This setting is optional. If omitted, defaults to "true" (masking enabled).
62+
LANGFUSE_MASK_MESSAGES: "true"

components/runners/claude-code-runner/observability.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,62 @@
5555
)
5656

5757

58+
def _privacy_masking_function(data: Any, **kwargs) -> Any:
59+
"""Mask sensitive user inputs and outputs while preserving usage metrics.
60+
61+
This function redacts message content (user prompts and assistant responses)
62+
to prevent logging potentially sensitive data, while preserving:
63+
- Usage metrics (token counts, costs)
64+
- Metadata (model, turn number, timestamps)
65+
- Session identifiers
66+
67+
Controlled by LANGFUSE_MASK_MESSAGES environment variable:
68+
- "true" (default): Redact all message content for privacy
69+
- "false": Allow full message logging (use only in dev/testing)
70+
71+
Args:
72+
data: Data to potentially mask (string, dict, list, or other)
73+
**kwargs: Additional context (unused but required by Langfuse API)
74+
75+
Returns:
76+
Masked data with same structure as input
77+
"""
78+
if isinstance(data, str):
79+
# Redact string content (likely message text)
80+
# Short strings (< 50 chars) might be metadata, keep them
81+
if len(data) > 50:
82+
return "[REDACTED FOR PRIVACY]"
83+
return data
84+
elif isinstance(data, dict):
85+
# Recursively process dict, preserving structure
86+
masked = {}
87+
for key, value in data.items():
88+
# Preserve usage and metadata fields - these don't contain sensitive data
89+
if key in ("usage", "usage_details", "metadata", "model", "turn",
90+
"input_tokens", "output_tokens", "cache_read_input_tokens",
91+
"cache_creation_input_tokens", "total_tokens", "cost_usd",
92+
"duration_ms", "duration_api_ms", "num_turns", "session_id",
93+
"tool_id", "tool_name", "is_error", "level"):
94+
masked[key] = value
95+
# Redact content fields that may contain user data
96+
elif key in ("content", "text", "input", "output", "prompt", "completion"):
97+
if isinstance(value, str) and len(value) > 50:
98+
masked[key] = "[REDACTED FOR PRIVACY]"
99+
else:
100+
# Short values might be metadata/enums, recurse
101+
masked[key] = _privacy_masking_function(value)
102+
else:
103+
# Recursively process other fields
104+
masked[key] = _privacy_masking_function(value)
105+
return masked
106+
elif isinstance(data, list):
107+
# Recursively process list items
108+
return [_privacy_masking_function(item) for item in data]
109+
else:
110+
# Preserve other types (numbers, booleans, None, etc.)
111+
return data
112+
113+
58114
class ObservabilityManager:
59115
"""Manages Langfuse observability for Claude sessions.
60116
"""
@@ -128,9 +184,25 @@ async def initialize(self, prompt: str, namespace: str, model: str = None) -> bo
128184
return False
129185

130186
try:
131-
# Initialize client
187+
# Determine if message masking should be enabled
188+
# Default: MASK messages (privacy-first approach)
189+
# Set LANGFUSE_MASK_MESSAGES=false to explicitly disable masking (dev/testing only)
190+
mask_messages_env = os.getenv("LANGFUSE_MASK_MESSAGES", "true").strip().lower()
191+
enable_masking = mask_messages_env not in ("false", "0", "no")
192+
193+
if enable_masking:
194+
logging.info("Langfuse: Privacy masking ENABLED - user messages and responses will be redacted")
195+
mask_fn = _privacy_masking_function
196+
else:
197+
logging.warning("Langfuse: Privacy masking DISABLED - full message content will be logged (use only for dev/testing)")
198+
mask_fn = None
199+
200+
# Initialize client with optional masking
132201
self.langfuse_client = Langfuse(
133-
public_key=public_key, secret_key=secret_key, host=host
202+
public_key=public_key,
203+
secret_key=secret_key,
204+
host=host,
205+
mask=mask_fn
134206
)
135207

136208
# Build metadata with model information

components/runners/claude-code-runner/tests/test_observability.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import logging
66
from unittest.mock import Mock, patch
7-
from observability import ObservabilityManager
7+
from observability import ObservabilityManager, _privacy_masking_function
88

99

1010
@pytest.fixture
@@ -151,10 +151,12 @@ async def test_init_successful(self, mock_langfuse_class, mock_propagate, manage
151151
assert manager.langfuse_client is not None
152152
assert manager._propagate_ctx is not None
153153

154+
# Verify Langfuse client was initialized with privacy masking enabled (default)
154155
mock_langfuse_class.assert_called_once_with(
155156
public_key="pk-lf-public",
156157
secret_key="sk-lf-secret",
157158
host="http://localhost:3000",
159+
mask=_privacy_masking_function,
158160
)
159161

160162
# Verify propagate_attributes was called

0 commit comments

Comments
 (0)