
Instrument Logs
- 122 installs
- 70 repo stars
- Updated August 4, 2026
- posthog/ai-plugin
instrument-logs is a Claude Code skill for ai & agent building.
About
instrument-logs is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- instrument-logs
- AI & Agent Building
- AI-coding skill
Instrument Logs by the numbers
- 122 all-time installs (skills.sh)
- +5 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #3,777 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/posthog/ai-plugin --skill instrument-logsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 122 |
|---|---|
| repo stars | ★ 70 |
| Last updated | August 4, 2026 |
| Repository | posthog/ai-plugin ↗ |
How do I helps with ai & agent building tasks during AI-assisted development.?
Helps with ai & agent building tasks during AI-assisted development.
Who is it for?
Best when you're working on ai & agent building and need structured help with instrument logs.
Skip if: Teams with no ai & agent building needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to helps with ai & agent building tasks during AI-assisted development., or when instrument-logs is a claude code skill for ai & agent building.
What you get
Structured output aligned to instrument-logs: instrument-logs, AI & Agent Building.
Files
Add PostHog log capture
Use this skill to add PostHog log capture for new or changed code. Use it after implementing features or reviewing PRs to ensure meaningful log events are captured with structured properties. If PostHog log export is not yet configured, this skill also covers initial OTLP exporter setup. Supports any platform or language.
Supported platforms: Next.js, Node.js, Python, Go, Java, Datadog, Android, React Native, iOS, and any language via OpenTelemetry.
Instructions
Follow these steps IN ORDER:
STEP 1: Analyze the codebase and detect the platform.
- Detect the language, framework, and existing logging setup.
- Look for dependency files and project files (package.json, Podfile, Package.swift, requirements.txt, go.mod, pom.xml, etc.).
- Look for log libraries (winston, pino, logging module, logrus, log4j, serilog, os_log, Logger, etc.).
- Look for lockfiles (pnpm-lock.yaml, package-lock.json, yarn.lock, bun.lockb, go.sum, Podfile.lock, Package.resolved, etc.) to determine the package manager.
- Check for existing PostHog log export setup. If the OTLP exporter is already configured, skip to STEP 5 to add log capture for new code.
STEP 2: Research log capture. (Skip if PostHog log export is already configured.) 2.1. Find the reference file below that matches the detected platform — it is the source of truth for OTLP exporter configuration and integration with existing logging. Read it now. 2.2. If no reference matches, use the "Other Languages" reference as a fallback — it covers the generic OpenTelemetry approach.
STEP 3: Install dependencies. (Skip if PostHog log export is already configured.)
- Install the OpenTelemetry SDK and OTLP exporter packages for the detected platform.
- Do not manually edit dependency files — use the package manager's install command.
- Always install packages as a background task. Don't await completion; proceed with other work immediately.
STEP 4: Configure the OTLP exporter. (Skip if PostHog log export is already configured.)
- PostHog logs use the OpenTelemetry protocol. Set up an OTLP exporter pointed at PostHog's ingest endpoint.
- For SDK-native log support such as Android, React Native, and iOS, follow the platform reference instead of adding a separate OTLP exporter.
- Follow the platform-specific reference for the exact configuration.
STEP 5: Integrate with existing logging.
- Add the PostHog log exporter alongside existing logging. Don't replace existing log handlers or outputs.
- Do not alter the fundamental architecture of existing files. Make additions minimal and targeted.
- You must read a file immediately before attempting to write it.
STEP 6: Add structured properties.
- Ensure logs include structured key-value properties for filtering and search in PostHog.
- Prefer structured log formats with key-value properties over plain text messages.
STEP 7: Set up environment variables.
- Check if the project already has PostHog environment variables configured (e.g. in
.env,.env.local, or framework-specific env files). If valid values already exist, skip this step. - If the PostHog API key is missing, use the PostHog MCP server's
projects-gettool to retrieve the project'sapi_token. If multiple projects are returned, ask the user which project to use. If the MCP server is not connected or not authenticated, ask the user for their PostHog project API key instead. - For the PostHog host URL, use
https://us.i.posthog.comfor US Cloud orhttps://eu.i.posthog.comfor EU Cloud. - For the OpenTelemetry endpoint, use
https://us.i.posthog.com/v1(US) orhttps://eu.i.posthog.com/v1(EU). - Write these values to the appropriate env file using the framework's naming convention.
- Reference these environment variables in code instead of hardcoding them.
Reference files
references/nextjs.md- Next.js logs installation - docsreferences/nodejs.md- Node.js logs installation - docsreferences/python.md- Python logs installation - docsreferences/go.md- Go logs installation - docsreferences/java.md- Java logs installation - docsreferences/datadog.md- Datadog logs installation - docsreferences/android.md- Android logs installation - docsreferences/react-native.md- React native logs installation - docsreferences/ios.md- Ios logs installation - docsreferences/flutter.md- Flutter logs installation - docsreferences/other.md- Other languages logs installation - docsreferences/start-here.md- Getting started with logs - docsreferences/search.md- Search logs - docsreferences/best-practices.md- Logging best practices - docsreferences/troubleshooting.md- Logs troubleshooting - docsreferences/link-session-replay.md- Link session replay - docsreferences/debug-logs-mcp.md- Debug logs with mcp - docs
Each platform reference contains specific OTLP configuration, SDK setup, and integration patterns. Find the one matching the user's stack.
Key principles
- Environment variables: Always use environment variables for PostHog keys and OpenTelemetry endpoints. Never hardcode them.
- Minimal changes: Add log export alongside existing logging. Don't replace or restructure existing logging code.
- OpenTelemetry: PostHog logs use the OpenTelemetry protocol. Configure an OTLP exporter pointed at PostHog's ingest endpoint unless the platform SDK provides native log capture.
- SDK-native logs: For Android, React Native, and iOS, use the SDK logger/capture APIs from the platform reference instead of adding a separate OTLP exporter.
- Structured logging: Prefer structured log formats with key-value properties over plain text messages.
Android Logs installation - Docs
The PostHog Android SDK has built-in support for capturing structured Logs from Android apps. The SDK handles the OTLP encoding, batching, on-disk persistence across app restarts, and lifecycle integration. You just call PostHog.logger.{trace,debug,info,warn,error,fatal}(...).
Manual capture only. Logs are emitted by your code. The SDK does not autocapture system log streams (Log.d,Logcat,Timber).
Minimum version:com.posthog:posthog-android@3.46.0or later. Bump the dependency in yourbuild.gradle(orbuild.gradle.kts) and re-sync.
1. 1
Install posthog-android
Required
If you haven't installed posthog-android yet, follow the Android SDK installation guide.
2. 2
Configure logs in your PostHogAndroidConfig
Required
Configure Logs through config.logs before calling PostHogAndroid.setup(...). All fields are optional; defaults are tuned for mobile (cellular bandwidth, battery, app lifecycle).
Kotlin
PostHog AI
val config = PostHogAndroidConfig(
apiKey = "<ph_project_token>",
host = "https://us.i.posthog.com",
).apply {
logs.serviceName = "my-app" // OTLP service.name – shown in the Logs UI
logs.environment = "production" // OTLP deployment.environment
logs.serviceVersion = "1.2.3" // OTLP service.version
}These resource attributes are captured at setup(...) and apply to every batch. Mutating config.logs.serviceName, environment, serviceVersion, or resourceAttributes after setup has no effect.
3. 3
Capture logs
Required
Use PostHog.logger for the per-level convenience API.
Kotlin
PostHog AI
import com.posthog.PostHog
import com.posthog.logs.PostHogLogSeverity
// Per-level convenience methods
PostHog.logger.info("checkout completed", mapOf("order_id" to "ord_789", "amount_cents" to 4999))
PostHog.logger.warn("payment retry", mapOf("attempt" to 2))
PostHog.logger.error("payment failed", mapOf("code" to "E001"))
// Generic entry point for a runtime severity (e.g. mapping a Timber priority)
PostHog.logger.log("rendered cart", severity = PostHogLogSeverity.DEBUG)Available severity levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL.
If you need W3C trace correlation, call PostHog.captureLog(...) directly and pass traceId, spanId, and traceFlags.
Kotlin
PostHog AI
PostHog.captureLog(
"payment failed",
severity = PostHogLogSeverity.ERROR,
attributes = mapOf("code" to "PAY_3001"),
traceId = "4bf92f3577b34da6a3ce929d0e0e4736",
spanId = "00f067aa0ba902b7",
traceFlags = 0x01,
)Records are buffered, batched, persisted to disk, and flushed automatically – every 30 seconds, when the buffer hits the threshold, when the app moves to the background, or on PostHog.flush(). flush() drains events, Session Replay, and Logs together.
Each record is automatically tagged with the current distinct ID, session ID, current screen, app foreground/background state, and active Feature Flags at the moment of capture.
From Java:
Java
PostHog AI
import com.posthog.PostHog;
import com.posthog.logs.PostHogLogSeverity;
import java.util.Map;
PostHog.Companion.getLogger().info("checkout opened", null);
PostHog.Companion.getLogger().error(
"payment failed",
Map.of("amount_cents", 1999, "currency", "USD")
);4. 4
Test your setup
Recommended
1. Capture a test log from your app:
Kotlin
PostHog AI
PostHog.logger.info("hello from Android")
PostHog.flush()2. Open the PostHog Logs UI. 3. Filter by service.name = 'my-app' (or whatever value you set above).
You should see your record arrive within a few seconds.
5. 5
Tune buffering, rate cap, and resource attributes
Optional
The logs config has knobs for high-volume apps:
Kotlin
PostHog AI
val config = PostHogAndroidConfig(apiKey = "<ph_project_token>").apply {
logs.serviceName = "my-app"
logs.flushIntervalSeconds = 5 // default 30
logs.maxBufferSize = 200 // default 1000
logs.maxBatchSize = 50 // default 50
logs.flushAt = 20 // default 20
logs.rateCapMaxLogs = 5000 // default 500
logs.rateCapWindowSeconds = 60 // default 10
logs.resourceAttributes = mapOf("host.name" to "device-01")
}
PostHogAndroid.setup(this, config)Full configuration reference:
| Field | Default | What it does |
|---|---|---|
| serviceName | app package id | OTLP service.name resource attribute |
| serviceVersion | BuildConfig.VERSION_NAME | OTLP service.version resource attribute |
| environment | null | OTLP deployment.environment resource attribute |
| resourceAttributes | {} | Extra OTLP resource attributes (SDK keys win on collision) |
| flushIntervalSeconds | 30 | Periodic flush interval |
| flushAt | 20 | Buffer threshold that triggers an automatic flush |
| maxBatchSize | 50 | Max records per outbound POST (halved on 413) |
| maxBufferSize | 1000 | Max records held on disk before FIFO eviction |
| rateCapMaxLogs | 500 | Max records per rateCapWindowSeconds window. Set to 0 to disable. |
| rateCapWindowSeconds | 10 | Rate-cap tumbling window length |
serviceName, serviceVersion, environment, resourceAttributes, flushAt, and maxBatchSize are captured at setup(...); mutating them later has no effect. flushIntervalSeconds, maxBufferSize, and rate-cap fields are re-read at runtime. Defaults are tuned for cellular-aware mobile apps. Raise rateCapMaxLogs and maxBufferSize for high-volume scenarios.
6. 6
Filter or redact with beforeSend
Optional
beforeSend runs synchronously before the rate cap, so dropped records don't consume the per-window budget. Use it for redaction, sampling, or filtering by level. Each hook receives an immutable PostHogLogRecord and returns either a (possibly modified) record or null to drop it.
Kotlin
PostHog AI
config.logs.addBeforeSend { record ->
// Drop debug logs in production
if (record.level == PostHogLogSeverity.DEBUG) return@addBeforeSend null
// Redact secrets in the body
record.copy(body = record.body.replace(Regex("api_key=\\S+"), "api_key=[REDACTED]"))
}Call addBeforeSend multiple times to compose a chain – hooks are evaluated left-to-right (registration order). Returning null from any hook short-circuits and drops the record. A hook that throws is treated the same as returning null (the record is dropped, the exception is logged via the SDK's internal debug logger). Returning a record with a blank body also drops the record.
addBeforeSend and removeBeforeSend are live – added or removed hooks take effect on the next captureLog call.
From Java, register a PostHogBeforeSendLog SAM:
Java
PostHog AI
config.getLogs().addBeforeSend(record ->
record.getBody().contains("secret") ? null : record
);8. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Logging best practices - Docs
Most logging is bad. Not because people don't log enough. They log too much of the wrong things and too little of the right things. The result is millions of lines that are expensive to store and useless to query.
This guide covers what actually makes logs useful in production systems. PostHog ingests logs via OpenTelemetry (OTLP), so the patterns here are built around OTel's structured logging model: resource attributes, log attributes, and trace context.
This guide covers:
- Centralize your logs
- Logging requests, not code
- Structured logging
- Cardinality and dimensionality
- Business context and OTel attributes
- Building wide events
- Log levels
- Sampling
- Trace and session context
- Schema evolution
- What not to log
- Automatic PII scrubbing
- Checklist
Centralize your logs
Centralizing your logs makes it possible to search across all your services in one place.
With PostHog, your logs live alongside your Product Analytics, Session Replays, and Feature Flags, so you can go from a log line to a user's session to the flag variant they were on without switching tools.
If you're already using posthog.capture(), you might wonder how logs differ from events. The key distinction is:
- Events track what the user did (e.g. clicks, signups, purchases, feature usage)
- Logs track what the system did (e.g. API requests, errors, retries, timeouts, configuration failures)
If you've been capturing things like database_connection_failed or stripe_api_timeout as PostHog events, those belong in logs instead.
Log what happened to requests, not what your code is doing
This is the single most important shift you can make.
PostHog AI
logger.info("Entering payment processing")
logger.info("Validating card details")
logger.info("Calling Stripe API")
logger.info("Stripe API returned")
logger.info("Updating database")
logger.info("Payment complete")Six log lines, none of them useful in production at INFO level. They tell you what the code does (you already know that, you wrote it), not what happened to a specific request.
Step-level logs aren't universally wrong. They're valuable at DEBUG level for diagnosing race conditions, understanding ordering in concurrent systems, or tracing through complex state machines. The point is: don't make them your default.
Your INFO\-level logs should be wide events. Your DEBUG\-level logs can be as granular as you need, turned on selectively when you're actively investigating.
Instead, emit one rich log per request per service:
JSON
PostHog AI
{
"event": "payment.completed",
"duration_ms": 342,
"posthogDistinctId": "user_abc123",
"order_id": "ord_789",
"amount_cents": 4999,
"currency": "USD",
"payment_method": "card",
"provider": "stripe",
"provider_latency_ms": 287,
"retry_count": 0,
"feature_flags": ["new_checkout_flow"],
"subscription_tier": "pro"
}One line. Everything you need to debug, alert on, or analyze, all in one place. This is a wide event (sometimes called a canonical log line), and it's the foundation of useful logging.
This pattern works cleanly for request-response services. For long-running processes, event-driven architectures, or workflows that span multiple services over minutes or hours, a pure single-event approach is less practical.
In those cases, use a hybrid: emit a wide event at each meaningful stage boundary (job started, stage completed, job finished), with each event carrying the full accumulated context up to that point. You still get the benefits of wide events without relying on a single emit that might never fire.
Use structured logging
Plain text logs are optimized for writing, not querying. Structured logs (JSON key-value pairs) are the opposite. They're queryable, filterable, and machine-readable.
Bad:
PostHog AI
Payment failed for user abc123 - Stripe error: card_declined (amount: $49.99)Good:
JSON
PostHog AI
{
"event": "payment.failed",
"posthogDistinctId": "user_abc123",
"error_type": "card_declined",
"provider": "stripe",
"amount_cents": 4999
}The structured version lets you query "all card\_declined errors for pro-tier users in the last hour" without regex. The plain text version requires you to hope your string parsing doesn't break on edge cases.
Structured logs in PostHog
PostHog's log search works across all fields in structured logs, so the more context you include, the more useful your search and filtering becomes. Every key-value pair is a field you can filter on.
Think in cardinality and dimensionality
Two concepts that separate useful logs from noise. You want both high cardinality and high dimensionality. One wide event with 50 fields tells you more than 50 separate log lines with three fields each.
What is cardinality?
Cardinality is the number of unique values a field has. posthogDistinctId has high cardinality (millions of unique values). log_level has low cardinality (5 values). High-cardinality fields are what enable you to debug specific requests and users.
Some teams avoid high-cardinality fields because older logging tools can't handle them efficiently. Modern columnar databases (like ClickHouse, which PostHog uses under the hood) handle high cardinality just fine. Don't let outdated tooling concerns stop you from logging the fields that matter.
What is dimensionality?
Dimensionality is the number of fields per log event. A log with three fields (timestamp, level, message) has low dimensionality. A wide event with 30+ fields has high dimensionality.
High dimensionality is what makes wide events powerful. Instead of scattering context across dozens of log lines, you pack it all into one event. This means every query can filter, group, and correlate across all those fields simultaneously.
Include business context
Technical context (status codes, latency, error types) is necessary but insufficient. Add the business context that turns debugging into understanding:
- Who: user ID, account type, subscription tier, organization
- What: order ID, cart contents, item count, Feature Flags
- Where: service name, deployment version, region
- How: payment method, auth provider, API version
- How much: amount, quantity, retry count
This lets you move from "500 errors spiked" to "500 errors spiked for enterprise users using the new checkout flow with coupon codes."
In OpenTelemetry, this context splits into two layers.
1. Resource attributes are set once when your service starts. They describe the service itself: service.name, deployment.environment, service.version, cloud.region. Every log from that process automatically includes them. 2. Log attributes are set per event. They describe what happened in that specific request: posthogDistinctId, order_id, payment_method, duration_ms.
Correlate with Product Analytics
If you're using PostHog for Product Analytics, the business context in your logs can match the properties on your events. This means you can go from a log search result straight to seeing how that user behaves in your product, and vice versa.
Build events throughout the request lifecycle
Don't emit 15 separate logs as a request moves through your code. Instead, accumulate context onto a single event and emit it once when the request completes.
The implementation details vary by language, but the pattern is always the same. These examples use the OpenTelemetry APIs from the installation guide:
Python
Python's standard logging module with the extra parameter. The OpenTelemetry SDK (configured in the installation guide) picks up these attributes automatically.
Python
PostHog AI
import logging
logger = logging.getLogger(__name__)
def handle_checkout(request):
attrs = {
"event": "checkout",
"posthogDistinctId": request.user.id,
"subscription_tier": request.user.tier,
}
cart = get_cart(request.user)
attrs.update({
"item_count": len(cart.items),
"cart_total_cents": cart.total_cents,
})
try:
payment = process_payment(cart)
attrs.update({
"payment_method": payment.method,
"provider": payment.provider,
"provider_latency_ms": payment.latency_ms,
"status": "success",
})
logger.info("checkout completed", extra=attrs)
except PaymentError as e:
attrs.update({"status": "failed", "error_type": e.code})
logger.error("checkout completed", extra=attrs)
raiseNode.js
The OpenTelemetry Logs API with logger.emit(). Attributes are passed as a dictionary on each log record. See the installation guide for SDK setup.
JavaScript
PostHog AI
import { logs } from "@opentelemetry/api-logs";
const logger = logs.getLogger("my-app");
function handleCheckout(req, res) {
const attrs = {
event: "checkout",
posthogDistinctId: req.user.id,
subscription_tier: req.user.tier,
};
const cart = getCart(req.user);
Object.assign(attrs, { item_count: cart.items.length, cart_total_cents: cart.totalCents });
try {
const payment = processPayment(cart);
Object.assign(attrs, {
payment_method: payment.method,
provider: payment.provider,
provider_latency_ms: payment.latencyMs,
status: "success",
});
logger.emit({ severityText: "INFO", body: "checkout completed", attributes: attrs });
} catch (e) {
Object.assign(attrs, { status: "failed", error_type: e.code });
logger.emit({ severityText: "ERROR", body: "checkout completed", attributes: attrs });
throw e;
}
}Go
Go's standard slog package, bridged to OpenTelemetry via otelslog (configured in the installation guide). Each slog.With() call returns a new logger with additional attributes.
Go
PostHog AI
func HandleCheckout(w http.ResponseWriter, r *http.Request) {
log := slog.With(
"event", "checkout",
"posthogDistinctId", r.Context().Value("posthogDistinctId"),
)
cart, _ := getCart(r.Context())
log = log.With(
"item_count", len(cart.Items),
"cart_total_cents", cart.TotalCents,
)
payment, err := processPayment(r.Context(), cart)
if err != nil {
log.With(
"status", "failed",
"error_type", err.Code,
).ErrorContext(r.Context(), "checkout completed")
return
}
log.With(
"payment_method", payment.Method,
"provider", payment.Provider,
"provider_latency_ms", payment.LatencyMs,
"status", "success",
).InfoContext(r.Context(), "checkout completed")
}One log line at the end, containing everything. Each step accumulates attributes, and the final emit carries them all.
Watch out for context bloat
Only bind scalar values (strings, numbers, booleans) to your log context. If you accidentally attach a full API response, a large query result, or a serialized object, you'll hit payload size limits or memory issues. Log the fields you need for debugging, not entire data structures.
What if the process crashes?
If your application crashes before reaching the end of a request (segfault, OOM, power failure), the accumulated context never gets emitted. Make sure you have a global exception handler or finally block that flushes whatever context has been collected. For long-running background jobs, consider emitting a "started" log at the beginning and "checkpoint" logs at key milestones, so a crash doesn't mean total data loss.
Use log levels correctly
Log levels exist to control signal-to-noise ratio. Use them consistently:
| Level | Use for | Example |
|---|---|---|
| ERROR | Something failed and needs attention | Payment processing failed, database connection lost |
| WARN | Something unexpected that didn't cause failure | Retry succeeded on third attempt, deprecated API version used |
| INFO | Normal operations worth recording | Request completed, user signed up, deployment finished |
| DEBUG | Detailed info for active debugging | Cache hit/miss ratios, query plans, intermediate state |
Two rules of thumb:
The noisy ERROR trap
1. If you're logging at ERROR, someone should eventually act on it. If no one ever looks at an error log, it's not an error. It's noise. 2. DEBUG logs should be off in production by default. Turn them on for specific services or requests when actively investigating.
Sample strategically
At scale, logging everything is expensive and unnecessary. Use tail sampling. Make sampling decisions after a request completes, based on the outcome:
- Keep 100% of errors and exceptions
- Keep 100% of requests that exceeded your p99 latency threshold
- Keep 100% of requests from important accounts or flagged sessions
This gives you full visibility into problems while keeping costs manageable. You lose nothing useful. The sampled successful requests are statistically representative.
Tail sampling is the ideal, but it's genuinely hard to implement well. Your logging pipeline needs to buffer data in memory until a request completes, and in distributed systems you need consistent sampling decisions across services for the same trace. This is typically handled by an OpenTelemetry Collector with a tail sampling processor, but configuring it correctly takes real effort.
If your infrastructure doesn't support tail sampling yet, head sampling (randomly keeping a fixed percentage of requests up front) is a pragmatic starting point. It's less precise (you'll drop some errors and keep some boring requests), but it's better than logging everything or nothing. You can always move to tail sampling later.
How much does log storage cost in PostHog?
PostHog Logs is billed by GB ingested per month with volume-based pricing. Use the calculator on the pricing page for a full breakdown.
Add trace and session context
Isolated logs are hard to correlate. Adding trace IDs and session IDs connects individual log events to the broader request journey.
Pick a library that supports structured output, async/buffered writes, and low per-call overhead. If you're using the OpenTelemetry SDK, the OTel log bridge adds minimal overhead on top of your chosen library, so the library itself is the bottleneck, not the export pipeline.
When in doubt, benchmark your logging path under realistic load before shipping to production.
Since PostHog uses OpenTelemetry, trace context propagation is automatic. Your logs are already correlated by trace ID if you have the OTel SDK configured. If you're also using PostHog for Product Analytics or Session Replay, you can go further and link your logs to Session Replays, giving you the user's full experience alongside your backend logs.
Link logs to Session Replays
By adding a PostHog session ID and distinct ID to your log attributes, you can jump directly from a log line to the user's Session Replay. See the Session Replay linking guide to set this up.
Treat your log schema like an API contract
Once you adopt wide events, your field names and value formats become dependencies. Dashboards, alerts, and saved searches all break silently when someone renames error_type to err_code or changes duration_ms from an integer to a string. Treat changes to your log schema the same way you'd treat changes to a public API: communicate them, deprecate before removing, and avoid breaking existing consumers.
What not to log
Some things should never appear in your logs:
- Secrets: API keys, passwords, tokens, credit card numbers. If you log these by accident, you now have a security incident and a logging problem.
- Request and response bodies: Logging full payloads is one of the fastest ways to blow up storage costs and accidentally capture PII, auth tokens, or sensitive user data. Log the metadata (status code, content length, duration), not the body.
- Personal data you don't need: Full email addresses, IP addresses, or other PII beyond what's required for debugging. If you need to correlate logs to a user but can't store raw identifiers, hash or tokenize them. Check your GDPR, HIPAA, or other compliance requirements, as even fields like
posthogDistinctIdoremailmay need masking depending on your jurisdiction. - High-frequency health checks: Load balancer pings and liveness probes generate massive volume with zero debugging value. Exclude them.
- Unnecessary duplication: If a downstream service logs the same event, you don't always need to log it again upstream. That said, when you're debugging a production incident at 2am, having key context from downstream calls in your own service's logs can save you from correlating across multiple systems under pressure. The rule of thumb: don't log a play-by-play of every call you make, but do include the outcome and any data you'd need to debug without switching to another service's logs.
Automatic PII scrubbing
Even with good practices, sensitive data can slip into logs accidentally. PostHog can automatically redact a small set of common patterns from your log payloads at ingestion time, before anything is stored.
Automatic PII scrubbing is in closed beta
Automatic PII scrubbing is currently available to internal PostHog teams only while we measure its ingestion overhead. If you'd like early access, please reach out to us via in-app support.
Once you have access, enable it under **Project settings** → **Logs** → **PII scrubbing**. The toggle is off by default.
When enabled, the following patterns are detected in each log record's body and string-valued attributes, and replaced with {{REDACTED}}:
- Bearer tokens –
Bearer <token>style credentials. TheBearerprefix is preserved, so the redacted output looks likeBearer {{REDACTED}}. - Stripe secret keys – values matching
sk_live_*orsk_test_*followed by at least 20 alphanumeric characters. - Email addresses – standard
local@domain.tldshape.
Scrubbing runs as a single regex pass over the raw body string and over each string-valued attribute. It does not parse JSON, does not walk nested structures, and does not redact based on attribute or JSON key names – a value only gets scrubbed if it matches one of the three patterns above. resource_attributes, service_name, severity_text, trace IDs, and other metadata fields are not touched.
Scrubbing is permanent
Redaction happens at ingestion and cannot be reversed. Original values are not retained anywhere – this is not reversible hashing.
A few things this feature explicitly does not catch today:
- Payment card numbers / PANs. Raw or hyphenated digit runs (for example
4242 4242 4242 4242) are not redacted. - Secrets identified only by key name. A value under a key like
password,api_key, orauthorizationis only redacted if the value itself matches a pattern above. The key name alone is not enough. - Numbers or booleans inside JSON. Only string content is scanned; JSON number and boolean leaves are not redacted.
- Anything that doesn't look like one of the three patterns. Custom token formats, opaque session IDs, addresses, phone numbers, names, IPs, and so on pass through unchanged.
Treat automatic PII scrubbing as a safety net for accidental leaks, not as a substitute for avoiding sensitive data in your logs in the first place.
Logging checklist
Use this to audit your existing logging or as a starting point for a new service.
Structural requirements
- Logs are structured JSON key-value pairs, not plain text strings
- Each request emits one wide event at the end, not a trail of step-by-step messages
- Only scalar values (strings, numbers, booleans) are logged. No raw objects, large arrays, or full API response bodies
- Context is accumulated throughout the request lifecycle (e.g.,
cart_totaladded once calculated,payment_idadded later)
Business and trace context
- The "Who":
posthogDistinctId,org_id,account_tier, or equivalent - The "What":
order_id,transaction_id,feature_flag_variants, or equivalent - The "Where":
service.name,service.version,deployment.environmentset as OTel resource attributes - Trace IDs: OpenTelemetry
trace_idis attached so you can jump from logs to traces - Session IDs: PostHog
session_idis included to enable Session Replay linking
Levels and sampling
- Log levels are correct: INFO for request completion, WARN for retries or non-breaking issues, ERROR only if someone needs to act
- Health checks (
/healthz) and load balancer pings are excluded or sampled down - A sampling strategy is in place (or planned) for high-traffic services
- A
try/finallyor global error handler flushes log context if the process dies mid-request
Security and compliance
- No secrets: API keys, Bearer tokens, and passwords are scrubbed
- PII is masked: emails, physical addresses, and credit card numbers are hashed or removed per GDPR/HIPAA requirements
- Request/response bodies are not logged (to avoid capturing sensitive user data)
- Field names and value types are treated as a stable schema (changes are communicated)
- Consider enabling automatic PII scrubbing as a safety net for accidental leaks
- Link logs to Session Replays for full user context
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Datadog logs installation - Docs
If you're already using Datadog to collect logs, you can forward them to PostHog by configuring your existing Datadog log exporters (like the Datadog Agent) to send logs to PostHog's Datadog-compatible endpoint.
1. 1
Get your project token
Required
You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.
Important: Use your project token which starts withphc_. Do not use a personal token (which starts withphx_).
You can find your project token in Project Settings.
2. 2
Configure Datadog Agent
Required
Set the Datadog logs URL to point to PostHog's Datadog-compatible endpoint. The endpoint format is:
PostHog AI
https://us.i.posthog.com/i/v1/logs/datadog/<ph_project_token>For the Datadog Agent, set the DD_LOGS_CONFIG_LOGS_DD_URL environment variable:
Terminal
PostHog AI
export DD_LOGS_CONFIG_LOGS_DD_URL="https://us.i.posthog.com/i/v1/logs/datadog/<ph_project_token>"Alternatively, you can set this in your datadog.yaml configuration file:
YAML
PostHog AI
logs_config:
logs_dd_url: "https://us.i.posthog.com/i/v1/logs/datadog/<ph_project_token>"3. 3
Other Datadog log exporters
Optional
If you're using other Datadog log exporters or forwarders, configure them to send logs to the same endpoint:
PostHog AI
https://us.i.posthog.com/i/v1/logs/datadog/<ph_project_token>The endpoint accepts logs in the standard Datadog log format, so existing integrations should work without additional changes.
4. 4
Test your setup
Recommended
Once everything is configured, test that logs are flowing into PostHog:
1. Restart the Datadog Agent (or your log forwarder) to apply the configuration 2. Generate some log entries in your application 3. Check the PostHog Logs interface for your log entries 4. Verify the logs appear in your project
6. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Debug Logs with MCP - Docs
The PostHog MCP server gives AI agents direct access to your Logs. Ask your agent to search, filter, and analyze log data without leaving your code editor.
With MCP, your agents can:
- Search and filter logs – Query by severity level, service name, date range, and free text.
- Discover log attributes – List available attributes and their values to build targeted queries.
- Debug in context – Investigate production issues without switching tools.
- Correlate with traces – Use trace IDs and span IDs from log entries to follow request flows across services.
This works in any MCP client – Cursor, Codex, Claude Code, Windsurf, VS Code, and others.
Example prompts
Try these prompts with your MCP-enabled agent:
-
Show me all error logs from the last hour. -
What services are logging errors? Search for error logs from the payments service. -
Find logs related to trace ID abc123. -
What log attributes are available? Show me the values for the service.name attribute. -
Show me warning and error logs from the last 24 hours, excluding debug noise.
Logs tools
The MCP server provides four tools for working with logs:
| Tool | Description |
|---|---|
| logs-query | Search and query logs with filters for severity levels (trace, debug, info, warn, error, fatal), service names, date ranges, and free text. Supports pagination for large result sets. |
| logs-list-attributes | List available log attributes in your project to discover what you can filter on. Supports filtering by attribute type (log or resource). |
| logs-list-attribute-values | Get possible values for a specific log attribute. Find service names, log levels, or other attribute values before querying. |
| logs-count | Get a count of logs matching your filters. Use as a pre-flight check before logs-query – if the count exceeds 1,000 (the maximum limit), narrow your filters or shorten the date range first. |
A typical workflow:
1. Call logs-list-attributes to discover available filter attributes. 2. Call logs-list-attribute-values to find specific values (e.g., which service names exist). 3. Call logs-count to check how many logs match your filters before querying. 4. Call logs-query to search logs with the right filters.
Get started
See the MCP server documentation for setup instructions.
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Flutter Logs installation - Docs
The PostHog Flutter SDK has built-in support for capturing structured Logs from your Flutter app across mobile and web. The SDK handles the OTLP encoding, batching, and flushing — and on mobile, on-disk persistence across app restarts and app-lifecycle integration (web buffers in memory via posthog-js). You just call Posthog().captureLog(...) or Posthog().logger.{trace,debug,info,warn,error,fatal}(...).
Manual capture only. Logs are emitted by your code. The SDK does not autocapture system log streams (debugPrint, ordart:developer'slog).
Minimum version:posthog_flutter5.27.0or later (the release that adds Logs support). On mobile it pulls inposthog-android3.48.0or later automatically.
1. 1
Install posthog\_flutter
Required
If you haven't installed posthog_flutter yet, follow the steps below. For full details, see the Flutter SDK guide.
PostHog is available for install via Pub.
Configuration
Set your PostHog project token and enable automatic event tracking if you want the library to capture lifecycle events for you.
Remember that the application lifecycle events won't have any special context set for you by the time it is initialized. If you are using a self-hosted instance of PostHog you will need to have the public hostname or IP for your instance as well.
To start, add posthog_flutter to your pubspec.yaml:
pubspec.yaml
PostHog AI
# rest of your code
dependencies:
flutter:
sdk: flutter
posthog_flutter: ^5.26.0
# rest of your codeThen complete the setup for each platform:
For Session Replay and Surveys, you must set up the SDK manually by disabling the com.posthog.posthog.AUTO_INIT mode.Android setup
There are 2 ways of initializing the SDK, automatically and manually.
Automatically:
Add your PostHog configuration to your AndroidManifest.xml file located in the android/app/src/main:
android/app/src/main/AndroidManifest.xml
PostHog AI
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="your.package.name">
<application>
<!-- ... other configuration ... -->
<meta-data android:name="com.posthog.posthog.PROJECT_TOKEN" android:value="<ph_project_token>" />
<meta-data android:name="com.posthog.posthog.POSTHOG_HOST" android:value="https://us.i.posthog.com" /> <!-- usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com' -->
<!-- com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS is enabled by default since version 5.23.0 (previously named TRACK_APPLICATION_LIFECYCLE_EVENTS, which still works as an alias) -->
<meta-data android:name="com.posthog.posthog.DEBUG" android:value="true" />
</application>
</manifest>Or manually (more control and more configurations available):
Add your PostHog configuration to your AndroidManifest.xml file located in the android/app/src/main:
android/app/src/main/AndroidManifest.xml
PostHog AI
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="your.package.name">
<application>
<!-- ... other configuration ... -->
<meta-data android:name="com.posthog.posthog.AUTO_INIT" android:value="false" />
</application>
</manifest>In both cases, you'll also need to update the minimum Android SDK version to 23 in android/app/build.gradle:
android/app/build.gradle
PostHog AI
// rest of your config
defaultConfig {
minSdkVersion 23
// rest of your config
}
// rest of your configiOS setup
There are 2 ways of initializing the SDK, automatically and manually.
You'll need to have Cocoapods installed.
Automatically:
Add your PostHog configuration to the Info.plist file located in the ios/Runner directory:
ios/Runner/Info.plist
PostHog AI
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- rest of your configuration -->
<key>com.posthog.posthog.PROJECT_TOKEN</key>
<string><ph_project_token></string>
<key>com.posthog.posthog.POSTHOG_HOST</key>
<string>https://us.i.posthog.com</string>
<!-- com.posthog.posthog.CAPTURE_APPLICATION_LIFECYCLE_EVENTS is enabled by default since version 5.23.0 -->
<key>com.posthog.posthog.DEBUG</key>
<true/>
</dict>
</plist>Or manually (more control and more configurations available):
Add your PostHog configuration to the Info.plist file located in the ios/Runner directory:
ios/Runner/Info.plist
PostHog AI
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- rest of your configuration -->
<key>com.posthog.posthog.AUTO_INIT</key>
<false/>
</dict>
</plist>In both cases, you'll need to set the minimum platform version to iOS 13.0 in your Podfile:
ios/Podfile
PostHog AI
platform :ios, '13.0'
# rest of your configDart setup (For manual step only)
If you followed the automatic SDK setup, then there's no more configuration needed in Dart.
If you followed the manual SDK setup:
Dart
PostHog AI
import 'package:flutter/material.dart';
import 'package:posthog_flutter/posthog_flutter.dart';
Future<void> main() async {
// init WidgetsFlutterBinding if not yet
WidgetsFlutterBinding.ensureInitialized();
final config = PostHogConfig('<ph_project_token>');
config.debug = true;
// captureApplicationLifecycleEvents is enabled by default since version 5.23.0
config.host = 'https://us.i.posthog.com';
await Posthog().setup(config);
runApp(MyApp());
}Web setup
For Web, add your Web snippet (which you can find in your project settings) in the <header> of your web/index.html file:
web/index.html
PostHog AI
<!DOCTYPE html>
<html>
<head>
<!-- ... other head elements ... -->
<script async>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<ph_project_token>', {
api_host:'https://us.i.posthog.com', // 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
defaults: '2026-01-30',
})
</script>
</head>
<!-- other elements -->
</html>For more information please check: /docs/libraries/js
2. 2
Configure logs in your PostHogConfig
Required
Configure Logs through config.logsConfig before calling Posthog().setup(...). All fields are optional; unset fields fall back to the native defaults, which are tuned for mobile (cellular bandwidth, battery, app lifecycle).
Dart
PostHog AI
final config = PostHogConfig('<ph_project_token>');
config.host = 'https://us.i.posthog.com';
config.logsConfig.serviceName = 'my-app'; // OTLP service.name – shown in the Logs UI
config.logsConfig.environment = 'production'; // OTLP deployment.environment
config.logsConfig.serviceVersion = '1.2.3'; // OTLP service.version
await Posthog().setup(config);These resource attributes are captured at setup(...) and apply to every batch.
Web behavior. On Flutter Web, the SDK attaches to an already-initialized `posthog-js` instance, soconfig.logsConfigis not applied on web. Configure your log options in theposthog.init({...})call in yourweb/index.htmlinstead.captureLogandloggerstill work on web (they are forwarded toposthog-js), andbeforeSendstill runs (in Dart) on web. Web also requires a recentposthog-jsbuild that exposescaptureLog.
For example, set the same service identity on the posthog-js snippet in web/index.html:
HTML
PostHog AI
<script>
posthog.init('<ph_project_token>', {
api_host: 'https://us.i.posthog.com',
logs: {
serviceName: 'my-app',
environment: 'production',
serviceVersion: '1.2.3',
},
})
</script>See the JavaScript Logs installation guide for the full posthog-js logs config.
3. 3
Capture logs
Required
Use Posthog().logger for the per-level convenience API, or Posthog().captureLog for full control over level, attributes, and trace context.
Dart
PostHog AI
import 'package:posthog_flutter/posthog_flutter.dart';
// Per-level convenience methods
Posthog().logger.info('checkout completed', {'order_id': 'ord_789', 'amount_cents': 4999});
Posthog().logger.warn('payment retry', {'attempt': 2});
Posthog().logger.error('payment failed', {'code': 'E001'});
// Lower-level API for custom severity / trace context
Posthog().captureLog(
body: 'checkout failed',
level: PostHogLogSeverity.error,
attributes: {'order_id': 'ord_789', 'step': 'auth'},
traceId: '4bf92f3577b34da6a3ce929d0e0e4736', // optional W3C trace context (32 hex chars)
spanId: '00f067aa0ba902b7', // optional W3C span (16 hex chars)
traceFlags: 1,
);The per-level facade methods are trace, debug, info, warn, error, and fatal, each taking a String body and an optional Map<String, Object> of attributes. Available severity levels for captureLog are PostHogLogSeverity.trace, .debug, .info, .warn, .error, and .fatal.
The optional W3C trace fields (traceId, spanId, traceFlags) are available on captureLog only, not on the logger facade.
Records are buffered, batched, persisted to disk, and flushed automatically – every 30 seconds, when the buffer hits the threshold, when the app moves to the background, or on Posthog().flush(). flush() drains events, Session Replay, and Logs together.
Each record is automatically tagged with the current distinct ID, session ID, active feature flags, and (on mobile) the current screen and app foreground/background state at the moment of capture. On web, url.full is tagged instead of screen name and app state.
4. 4
Test your setup
Recommended
1. Capture a test log from your app:
Dart
PostHog AI
Posthog().logger.info('hello from Flutter');
Posthog().flush();2. Open the PostHog Logs UI. 3. Filter by service.name = 'my-app' (or whatever value you set above).
You should see your record arrive within a few seconds.
5. 5
Tune buffering, rate cap, and resource attributes
Optional
The logsConfig has knobs for high-volume apps:
Dart
PostHog AI
final config = PostHogConfig('<ph_project_token>');
config.logsConfig.serviceName = 'my-app';
config.logsConfig.flushInterval = Duration(seconds: 5); // default 30s
config.logsConfig.maxBufferSize = 200; // default 1000
config.logsConfig.maxBatchSize = 50; // default 50
config.logsConfig.flushAt = 20; // default 20
config.logsConfig.rateCapMaxLogs = 5000; // default 500
config.logsConfig.rateCapWindow = Duration(seconds: 60); // default 10s
config.logsConfig.resourceAttributes = {'host.name': 'device-01'};
await Posthog().setup(config);Full configuration reference:
| Field | Default | What it does |
|---|---|---|
| serviceName | app bundle id (iOS) / app namespace (Android) | OTLP service.name resource attribute |
| serviceVersion | app version | OTLP service.version resource attribute |
| environment | none | OTLP deployment.environment resource attribute |
| resourceAttributes | {} | Extra OTLP resource attributes |
| flushInterval | 30s | Periodic flush interval |
| flushAt | 20 | Buffer threshold that triggers an automatic flush |
| maxBatchSize | 50 | Max records per outbound POST |
| maxBufferSize | 1000 | Max records held on disk before FIFO eviction |
| rateCapMaxLogs | 500 | Max records per rateCapWindow. Set to 0 to disable. |
| rateCapWindow | 10s | Rate-cap window length |
Defaults are tuned for cellular-aware mobile apps. Raise rateCapMaxLogs and maxBufferSize for high-volume scenarios.
On web, these fields are not applied – configure them in yourposthog.init({...})call inweb/index.htmlinstead.
6. 6
Filter or redact with beforeSend
Optional
Use config.logsConfig.beforeSend for redaction, sampling, or filtering by level. It is a List<BeforeSendLogCallback>, where each callback is a FutureOr<PostHogLogRecord?> Function(PostHogLogRecord). Callbacks run in Dart on all platforms (including web), evaluated left-to-right. Each callback receives a mutable PostHogLogRecord (with mutable body, level, and attributes) and returns either the (possibly mutated) record or null to drop it. Callbacks can be synchronous or asynchronous.
Dart
PostHog AI
config.logsConfig.beforeSend = [
(record) {
// Drop debug logs in production
if (record.level == PostHogLogSeverity.debug) return null;
// Redact a sensitive attribute
record.attributes?.remove('password');
return record;
},
// Compose a chain – callbacks run left-to-right
(record) => record.body.contains('secret') ? null : record,
];Returning null from any callback short-circuits and drops the record. Setting record.body to an empty or whitespace-only string also drops the record. A callback that throws is logged and the record is dropped (fail-closed).
8. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Go logs installation - Docs
1. 1
Install OpenTelemetry packages
Required
Terminal
PostHog AI
go get go.opentelemetry.io/otel/sdk/log
go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp2. 2
Get your project token
Required
You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.
Important: Use your project token which starts withphc_. Do not use a personal API key (which starts withphx_).
You can find your project token in Project Settings.
3. 3
Configure the SDK
Required
Set up the OpenTelemetry SDK to send logs to PostHog.
Go
PostHog AI
package main
import (
"os"
"context"
"log"
"log/slog"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
"go.opentelemetry.io/contrib/bridges/otelslog"
otellog "go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/log/global"
)
func main() {
ctx := context.Background()
// Create OTLP HTTP exporter
exporter, err := otlploghttp.New(ctx,
otlploghttp.WithEndpoint("us.i.posthog.com"),
otlploghttp.WithURLPath("/i/v1/logs"),
otlploghttp.WithHeaders(map[string]string{
"Authorization": "Bearer <ph_project_token>",
}),
)
if err != nil {
panic(err)
}
// you could also set this outside your application
os.Setenv("OTEL_SERVICE_NAME", "my-service")
stdoutExporter, _ := stdoutlog.New()
// Create logger provider
loggerProvider := otellog.NewLoggerProvider(
otellog.WithProcessor(otellog.NewBatchProcessor(exporter)),
// optional, also log to stdout
otellog.WithProcessor(otellog.NewSimpleProcessor(stdoutExporter)),
)
defer func() {
loggerProvider.Shutdown(context.Background())
}()
global.SetLoggerProvider(loggerProvider)
slog.SetDefault(otelslog.NewLogger(""))
log.Println("this is a log line")
}Alternatively, you can pass the API key as a query parameter by modifying the URL path:
Go
PostHog AI
otlploghttp.WithURLPath("/i/v1/logs?token=<ph_project_token>")4. 4
Use OpenTelemetry logging
Required
Now you can start logging with OpenTelemetry:
Go
PostHog AI
import (
"go.opentelemetry.io/otel/log"
)
logger := otel.GetLoggerProvider().Logger("my-app")
logger.Info(ctx, "User action",
log.String("userId", "123"),
log.String("action", "login"),
)5. 5
Test your setup
Recommended
Once everything is configured, test that logs are flowing into PostHog:
1. Send a test log from your application 2. Check the PostHog Logs interface for your log entries 3. Verify the logs appear in your project
7. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
iOS Logs installation - Docs
The PostHog iOS SDK has built-in support for capturing structured Logs from iOS, macOS, tvOS, watchOS, and visionOS apps. The SDK handles OTLP encoding, batching, on-disk persistence across app restarts, and lifecycle integration. You just call PostHogSDK.shared.captureLog(...) or PostHogSDK.shared.logger?.{trace,debug,info,warn,error,fatal}(...).
Manual capture only. Logs are emitted by your code. The SDK does not autocapture system log streams (os_log,Logger,
Minimum version:posthog-ios@3.58.0or later. Runpod update PostHog(CocoaPods) or update the package version in Xcode (Swift Package Manager).
1. 1
Install posthog-ios
Required
If you haven't installed posthog-ios yet, follow the steps below. For full details, see the iOS SDK guide.
PostHog is available through CocoaPods or you can add it as a Swift Package Manager based dependency.
CocoaPods
Podfile
PostHog AI
pod "PostHog", "~> 3.59.3"Swift Package Manager
Add PostHog as a dependency in your Xcode project "Package Dependencies" and select the project target for your app, as appropriate.
For a Swift Package Manager based project, add PostHog as a dependency in your Package.swift file's Package dependencies section:
Package.swift
PostHog AI
dependencies: [
.package(url: "https://github.com/PostHog/posthog-ios.git", from: "3.59.3")
],and then as a dependency for the Package target utilizing PostHog:
Package.swift
PostHog AI
.target(
name: "myApp",
dependencies: [.product(name: "PostHog", package: "posthog-ios")]),Configuration
Configuration is done through the PostHogConfig object. Here's a basic configuration example to get you started.
You can find more advanced configuration options in the configuration page.
UIKit
Swift
PostHog AI
import Foundation
import PostHog
import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
let POSTHOG_PROJECT_TOKEN = "<ph_project_token>"
// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
let POSTHOG_HOST = "https://us.i.posthog.com"
let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST)
PostHogSDK.shared.setup(config)
return true
}
}SwiftUI
Swift
PostHog AI
import SwiftUI
import PostHog
@main
struct YourGreatApp: App {
// Add PostHog to your app's initializer.
// If using UIApplicationDelegateAdaptor, see the UIKit tab.
init() {
let POSTHOG_PROJECT_TOKEN = "<ph_project_token>"
// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
let POSTHOG_HOST = "https://us.i.posthog.com"
let config = PostHogConfig(projectToken: POSTHOG_PROJECT_TOKEN, host: POSTHOG_HOST)
PostHogSDK.shared.setup(config)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}2. 2
Configure logs in your PostHogConfig
Required
Configure Logs through config.logs before calling setup(_:). All fields are optional; defaults are tuned for mobile (cellular bandwidth, battery, OS lifecycle).
Swift
PostHog AI
import PostHog
let config = PostHogConfig(projectToken: "<ph_project_token>", host: "https://us.i.posthog.com")
config.logs.serviceName = "my-app" // OTLP service.name – shown in the Logs UI
config.logs.environment = "production" // OTLP deployment.environment
config.logs.serviceVersion = "1.2.3" // OTLP service.version
PostHogSDK.shared.setup(config)These resource attributes are captured at setup(_:) and apply to every batch. Mutating config.logs after setup has no effect.
3. 3
Capture logs
Required
Use PostHogSDK.shared.logger for the per-level convenience API, or PostHogSDK.shared.captureLog for full control over level, attributes, and trace context.
Swift
PostHog AI
// Per-level convenience methods – Optional, since logger is created at setup
PostHogSDK.shared.logger?.info("checkout completed", attributes: ["order_id": "ord_789", "amount_cents": 4999])
PostHogSDK.shared.logger?.warn("payment retry", attributes: ["attempt": 2])
PostHogSDK.shared.logger?.error("payment failed", attributes: ["code": "E001"])
// Lower-level API for custom severity / trace context
PostHogSDK.shared.captureLog(
"checkout failed",
level: .error,
attributes: ["order_id": "ord_789", "step": "auth"],
traceId: "4bf92f3577b34da6a3ce929d0e0e4736", // optional W3C trace context (32 hex chars)
spanId: "00f067aa0ba902b7" // optional W3C span (16 hex chars)
)Available severity levels: .trace, .debug, .info, .warn, .error, .fatal.
Records are buffered, batched, persisted to disk, and flushed automatically – every 30 seconds, when the buffer hits the threshold, when the app moves to the background, or on PostHogSDK.shared.flush(). flush() drains events, Session Replay, and Logs together.
Each record is automatically tagged with the current distinct ID, session ID, current screen, app foreground/background state, and active Feature Flags at the moment of capture.
4. 4
Test your setup
Recommended
1. Capture a test log from your app:
Swift
PostHog AI
PostHogSDK.shared.logger?.info("hello from iOS")
PostHogSDK.shared.flush()2. Open the PostHog Logs UI. 3. Filter by service.name = 'my-app' (or whatever value you set above).
You should see your record arrive within a few seconds.
5. 5
Tune buffering, rate cap, and resource attributes
Optional
The logs config has knobs for high-volume apps:
Swift
PostHog AI
let config = PostHogConfig(projectToken: "<ph_project_token>")
config.logs.serviceName = "my-app"
config.logs.flushIntervalSeconds = 5 // default 30
config.logs.maxBufferSize = 200 // default 1000
config.logs.maxBatchSize = 50 // default 50
config.logs.flushAt = 20 // default 20
config.logs.rateCapMaxLogs = 5000 // default 500
config.logs.rateCapWindowSeconds = 60 // default 10
config.logs.resourceAttributes = ["host.name": "device-01"]
PostHogSDK.shared.setup(config)Full configuration reference:
| Field | Default | What it does |
|---|---|---|
| serviceName | bundle identifier | OTLP service.name resource attribute |
| serviceVersion | CFBundleShortVersionString | OTLP service.version resource attribute |
| environment | nil | OTLP deployment.environment resource attribute |
| resourceAttributes | [:] | Extra OTLP resource attributes (SDK keys win on collision) |
| flushIntervalSeconds | 30 | Periodic flush interval |
| flushAt | 20 | Buffer threshold that triggers an automatic flush |
| maxBatchSize | 50 | Max records per outbound POST (halved on 413) |
| maxBufferSize | 1000 | Max records held on disk before FIFO eviction |
| rateCapMaxLogs | 500 | Max records per rateCapWindowSeconds window. Set to 0 to disable. |
| rateCapWindowSeconds | 10 | Rate-cap tumbling window length |
All of the above are captured at setup(_:); mutating them later has no effect. Defaults are tuned for cellular-aware mobile apps. Raise rateCapMaxLogs and maxBufferSize for high-volume scenarios.
6. 6
Filter or redact with beforeSend
Optional
beforeSend runs synchronously before the rate cap, so dropped records don't consume the per-window budget. Use it for redaction, sampling, or filtering by level. Each block receives a mutable PostHogLogRecord and returns either the (possibly mutated) record or nil to drop it.
Swift
PostHog AI
config.logs.setBeforeSend({ record in
// Drop debug logs in production
if record.level == .debug { return nil }
// Redact secrets in the body
record.body = record.body.replacingOccurrences(
of: #"api_key=\S+"#,
with: "api_key=[REDACTED]",
options: .regularExpression
)
return record
})Pass an array (or a comma-separated list) of blocks to compose a chain – evaluated left-to-right. Returning nil from any block short-circuits and drops the record. Setting record.body to an empty string also drops the record.
From Objective-C, wrap each closure in a BoxedBeforeSendLogBlock:
objc
PostHog AI
[posthogConfig.logs setBeforeSend:@[
[[BoxedBeforeSendLogBlock alloc] initWithBlock:^PostHogLogRecord * _Nullable(PostHogLogRecord * record) {
return [record.body containsString:@"secret"] ? nil : record;
}]
]];8. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Java logs installation - Docs
1. 1
Install OpenTelemetry packages
Required
Add the following dependencies to your pom.xml:
XML
PostHog AI
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
<version>1.32.0</version>
</dependency>2. 2
Get your project token
Required
You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.
Important: Use your project token which starts withphc_. Do not use a personal API key (which starts withphx_).
You can find your project token in Project Settings.
3. 3
Configure the SDK
Required
Set up the OpenTelemetry SDK to send logs to PostHog.
Java
PostHog AI
import io.opentelemetry.api.logs.GlobalLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.exporter.otlp.logs.OtlpHttpLogRecordExporter;
SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
.addLogRecordProcessor(
BatchLogRecordProcessor.builder(
OtlpHttpLogRecordExporter.builder()
.setEndpoint("https://us.i.posthog.com/i/v1/logs")
.addHeader("Authorization", "Bearer <ph_project_token>")
.build()
).build()
)
.build();
GlobalLoggerProvider.set(loggerProvider);Alternatively, you can pass the API key as a query parameter:
Java
PostHog AI
OtlpHttpLogRecordExporter.builder()
.setEndpoint("https://us.i.posthog.com/i/v1/logs?token=<ph_project_token>")
.build()4. 4
Use OpenTelemetry logging
Required
Now you can start logging with OpenTelemetry:
Java
PostHog AI
import io.opentelemetry.api.logs.Logger;
Logger logger = GlobalLoggerProvider.get().get("my-app");
logger.logRecordBuilder()
.setBody("User action")
.setAttributes(Attributes.of(
AttributeKey.stringKey("userId"), "123",
AttributeKey.stringKey("action"), "login"
))
.emit();5. 5
Test your setup
Recommended
Once everything is configured, test that logs are flowing into PostHog:
1. Send a test log from your application 2. Check the PostHog Logs interface for your log entries 3. Verify the logs appear in your project
7. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Link session replay - Docs
Connecting your backend logs to frontend session replays provides complete visibility into the user journey, helping you understand the full context around issues in your application.
Why link to session replay?
By including session IDs and user identity in your logs, you can:
- See the full user journey: Navigate from a log entry directly to the session replay to see what the user was doing
- Debug issues faster: Quickly find and watch the exact session where an error or issue occurred
- Correlate logs with user actions: Match backend log events with actual user experience
- View related errors: See Error Tracking issues that occurred during the same session directly in the log details
Prerequisites
- A logging client installed on your backend
- The PostHog JavaScript SDK on your web frontend, or the React Native SDK in your mobile app
- Session replay enabled if you want to link to replays (you can still pass
posthogDistinctIdwithout session replay to link logs to a user profile)
Logs captured client-side: When you callposthog.captureLog/posthog.logger.*directly from the JavaScript web SDK or React Native SDK, the currentdistinct_idandsession_idare attached to every log record automatically. You only need the manual setup below when your backend emits the logs.
Implementation
To link logs to session replays, you need to pass the session ID and user identity from your frontend to your backend, then include them as log attributes.
Frontend: Get the session ID
In your frontend code, retrieve the current session ID and send it with your API requests:
PostHog AI
JavaScript
import posthog from 'posthog-js'
// Get the current session ID
const sessionId = posthog.getSessionId()
// Send it with your API request
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userInput,
sessionId: sessionId // Include session ID
})
})"React
import { posthog } from './posthog'
// Get the current session ID
const sessionId = posthog.getSessionId()
// Send it with your API request
const response = await fetch('https://api.example.com/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: userInput,
sessionId, // Include session ID
}),
})Backend: Include session ID and user identity in logs
Once you have the session ID, include it along with the user's identity using the sessionId and posthogDistinctId attributes. These examples assume you've already set up a logging client for your language.
PostHog AI
JavaScript
import { logs } from '@opentelemetry/api-logs'
const logger = logs.getLogger('my-app')
app.post('/api/chat', async (req, res) => {
const { message, sessionId } = req.body
const userId = req.userId // ... get your user ID
logger.emit({
severityText: 'info',
body: 'Chat request received',
attributes: {
posthogDistinctId: userId, // Links to PostHog user
sessionId: sessionId, // Links to session replay
endpoint: '/api/chat',
},
})
// ... handle the request
res.json({ success: true })
})Python
import logging
logger = logging.getLogger(__name__)
@app.route('/api/chat', methods=['POST'])
def chat():
data = request.json
message = data['message']
session_id = data.get('sessionId')
user_id = current_user.id
logger.info(
"Chat request received",
extra={
"posthogDistinctId": user_id, # Links to PostHog user
"sessionId": session_id, # Links to session replay
"endpoint": "/api/chat",
}
)
# ... handle the request
return jsonify({"success": True})Note: If you don't includeposthogDistinctId, logs won't be linked to a user. If you don't includesessionId, logs won't be linked to a session replay. You can use either or both independently.
Viewing linked replays
Once you've set up session linking, you can navigate from logs to their corresponding session replays:
1. In the logs view, click on the log entry you're interested in to open log details 2. In the log details view, click the View recording button to open the session replay 3. Watch the user's interaction in context alongside the backend logs
You can only view recordings for log entries that have an associated session ID.
This linking helps you correlate backend log events with actual frontend user behavior, enabling faster debugging and better understanding of issues as they occur in your application.
View related errors
When you click on a log entry that has a session ID, you can view related errors in the Related errors tab. This tab shows Error Tracking issues that occurred within the same session (within ±6 hours of the log timestamp).
This helps you debug issues by showing errors that happened around the same time as your log entry, giving you a more complete picture of what went wrong.
If no session ID is found in the log entry, the tab displays a message prompting you to link your logs to sessions.
See also
- Link logs to a person: same
posthogDistinctIdattribute, surfaced on the person profile's Logs tab. - Session replay installation
- Logs installation
- Search logs
- Error Tracking
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Next.js logs installation - Docs
1. 1
Install OpenTelemetry packages
Required
Terminal
PostHog AI
npm install @opentelemetry/sdk-logs @opentelemetry/exporter-logs-otlp-http @opentelemetry/api-logs @opentelemetry/resources2. 2
Get your project token
Required
You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.
Important: Use your project token which starts withphc_. Do not use a personal API key (which starts withphx_).
You can find your project token in Project Settings.
3. 3
Enable instrumentation in Next.js
Required
Note: For Next.js 15 and later, the instrumentation hook is enabled by default. You can skip this step if you're on Next.js 15+.
Add the following to your next.config.js (or next.config.mjs) to enable the instrumentation hook:
JavaScript
PostHog AI
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
instrumentationHook: true,
},
}
module.exports = nextConfig4. 4
Create the instrumentation file
Required
Create an instrumentation.ts (or instrumentation.js) file in the root of your project (or inside src/ if you use that folder).
typescript
PostHog AI
import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs'
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'
import { logs } from '@opentelemetry/api-logs'
import { resourceFromAttributes } from '@opentelemetry/resources'
// Create LoggerProvider outside register() so it can be exported and flushed in route handlers
export const loggerProvider = new LoggerProvider({
resource: resourceFromAttributes({ 'service.name': 'my-nextjs-app' }),
processors: [
new BatchLogRecordProcessor(
new OTLPLogExporter({
url: 'https://us.i.posthog.com/i/v1/logs',
headers: {
Authorization: 'Bearer <ph_project_token>',
'Content-Type': 'application/json',
},
})
),
],
})
export function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
logs.setGlobalLoggerProvider(loggerProvider)
}
}Note: TheloggerProvideris created outside ofregister()so it can be exported and used to flush logs in route handlers. This pattern is necessary because Route Handlers complete execution before batched logs have a chance to be sent to the collector. By exporting the provider, we can manually flush logs at the end of each request.
Important: The Content-Type: application/json header is required.Alternatively, you can pass the API key as a query parameter:
typescript
PostHog AI
new OTLPLogExporter({
url: 'https://us.i.posthog.com/i/v1/logs?token=<ph_project_token>',
headers: {
'Content-Type': 'application/json',
},
})5. 5
Use OpenTelemetry logging
Required
Now you can use OpenTelemetry logging in your server-side code (API routes, Server Components, etc.):
typescript
PostHog AI
import { SeverityNumber } from '@opentelemetry/api-logs'
import { after } from 'next/server'
import { loggerProvider } from '@/instrumentation'
const logger = loggerProvider.getLogger('my-nextjs-app')
export async function GET() {
logger.emit({
body: 'API request received',
severityNumber: SeverityNumber.INFO,
attributes: {
endpoint: '/api/example',
method: 'GET',
},
})
// Ensure logs are flushed before the serverless function freezes
after(async () => {
await loggerProvider.forceFlush()
})
return Response.json({ success: true })
}Important: Without callingforceFlush(), your logs may not be sent. Route Handlers complete execution before the OpenTelemetry batch processor has a chance to send logs to the collector. Theafter()function fromnext/serverruns code after the response is sent, ensuring logs are flushed before the serverless function freezes.
6. 6
Test your setup
Recommended
Once everything is configured, test that logs are flowing into PostHog:
1. Send a test log from your application 2. Check the PostHog Logs interface for your log entries 3. Verify the logs appear in your project
8. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Node.js logs installation - Docs
1. 1
Install OpenTelemetry packages
Required
Terminal
PostHog AI
npm install @opentelemetry/sdk-node @opentelemetry/exporter-logs-otlp-http @opentelemetry/api-logs @opentelemetry/resources @opentelemetry/sdk-logs2. 2
Get your project token
Required
You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.
Important: Use your project token which starts withphc_. Do not use a personal API key (which starts withphx_).
You can find your project token in Project Settings.
3. 3
Configure the SDK
Required
Set up the OpenTelemetry SDK to send logs to PostHog.
JavaScript
PostHog AI
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
import { resourceFromAttributes } from '@opentelemetry/resources';
const sdk = new NodeSDK({
resource: resourceFromAttributes({
'service.name': 'my-node-service',
}),
logRecordProcessor: new BatchLogRecordProcessor(
new OTLPLogExporter({
url: 'https://us.i.posthog.com/i/v1/logs',
headers: {
'Authorization': 'Bearer <ph_project_token>'
}
})
)
});
sdk.start();Alternatively, you can pass the API key as a query parameter:
JavaScript
PostHog AI
const sdk = new NodeSDK({
logRecordProcessor: new BatchLogRecordProcessor(
new OTLPLogExporter({
url: 'https://us.i.posthog.com/i/v1/logs?token=<ph_project_token>'
})
)
});4. 4
Use OpenTelemetry logging
Required
Now you can start logging with OpenTelemetry:
JavaScript
PostHog AI
import { logs } from '@opentelemetry/api-logs';
const logger = logs.getLogger('my-app');
// Log with different levels and attributes
logger.emit({ severityText: 'trace', body: 'log data', attributes: {'my_attribute': 'stringValue'} });
logger.emit({ severityText: 'warn', body: 'log data', attributes: {'warning_count': 3} });
logger.emit({ severityText: 'error', body: 'log data', attributes: {'json_attribute': [1,2,3]} });5. 5
Test your setup
Recommended
Once everything is configured, test that logs are flowing into PostHog:
1. Send a test log from your application 2. Check the PostHog Logs interface for your log entries 3. Verify the logs appear in your project
7. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Other languages logs installation - Docs
PostHog Logs works with any OpenTelemetry-compatible client. Check the OpenTelemetry documentation for your specific language or framework.
1. 1
Install OpenTelemetry packages
Required
The key requirements are:
- Use OTLP (OpenTelemetry Protocol) for log export over HTTP
- Send logs to your Logs endpoint (see configuration step below)
- Include your project token in the Authorization header or as a
?token=query parameter
Find the OpenTelemetry SDK for your language in the official registry.
2. 2
Get your project token
Required
You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.
Important: Use your project token which starts withphc_. Do not use a personal API key (which starts withphx_).
You can find your project token in Project Settings.
3. 3
Configure the SDK
Required
Configure your OpenTelemetry SDK to send logs to PostHog.
Endpoint:
PostHog AI
https://us.i.posthog.com/i/v1/logsAuthentication: Include your project token either as an Authorization header:
PostHog AI
Authorization: Bearer <ph_project_token>Or as a query parameter on the endpoint:
PostHog AI
https://us.i.posthog.com/i/v1/logs?token=<ph_project_token>4. 4
Test your setup
Recommended
Once everything is configured, test that logs are flowing into PostHog:
1. Send a test log from your application 2. Check the PostHog Logs interface for your log entries 3. Verify the logs appear in your project
6. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Python logs installation - Docs
1. 1
Install OpenTelemetry packages
Required
Terminal
PostHog AI
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp2. 2
Get your project token
Required
You'll need your PostHog project token to authenticate log requests. This is the same key you use for capturing events and exceptions with the PostHog SDK.
Important: Use your project token which starts withphc_. Do not use a personal API key (which starts withphx_).
You can find your project token in Project Settings.
3. 3
Configure the SDK
Required
Set up the OpenTelemetry SDK to send logs to PostHog.
Python
PostHog AI
from opentelemetry import logs
from opentelemetry.sdk.logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk.logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
# Configure the logger provider
logger_provider = LoggerProvider()
logs.set_logger_provider(logger_provider)
# Create OTLP exporter with API key in header
otlp_exporter = OTLPLogExporter(
endpoint="https://us.i.posthog.com/i/v1/logs",
headers={"Authorization": "Bearer <ph_project_token>"}
)
# Add processor
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(otlp_exporter)
)
# Get logger
logger = logs.get_logger("my-app")Alternatively, you can pass the API key as a query parameter:
Python
PostHog AI
otlp_exporter = OTLPLogExporter(
endpoint="https://us.i.posthog.com/i/v1/logs?token=<ph_project_token>"
)4. 4
Use OpenTelemetry logging
Required
Now you can start logging with OpenTelemetry:
Python
PostHog AI
import logging
# Configure logging to use OpenTelemetry
logging.basicConfig(level=logging.INFO)
logging.getLogger().addHandler(LoggingHandler())
# Use standard Python logging
logger = logging.getLogger("my-app")
logger.info("User action", extra={"userId": "123", "action": "login"})
logger.warning("Deprecated API used", extra={"endpoint": "/old-api"})
logger.error("Database connection failed", extra={"error": "Connection timeout"})5. 5
Test your setup
Recommended
Once everything is configured, test that logs are flowing into PostHog:
1. Send a test log from your application 2. Check the PostHog Logs interface for your log entries 3. Verify the logs appear in your project
7. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
React Native logs installation - Docs
PostHog's React Native SDK has built-in support for capturing structured logs. Unlike other languages where you wire OpenTelemetry directly, the SDK handles the OTLP encoding, batching, persistence, and lifecycle for you. You just call posthog.captureLog(...) or posthog.logger.{trace,debug,info,warn,error,fatal}(...).
JavaScript layer only. Logs are captured from the JavaScript side of your app. Native logs from your iOS or Android code (e.g.os_log,Log.d) are not collected.
Minimum version:posthog-react-native@4.44.0or later. Runnpx expo install posthog-react-native(Expo) or your package manager's equivalent to update.
1. 1
Install posthog-react-native
Required
If you haven't already, install and initialize posthog-react-native using the steps below. For full details, see the React Native SDK guide.
Our React Native enables you to integrate PostHog with your React Native project. For React Native projects built with Expo, there are no mobile native dependencies outside of supported Expo packages.
To install, add the posthog-react-native package to your project as well as the required peer dependencies.
Expo apps
Terminal
PostHog AI
npx expo install posthog-react-native expo-file-system expo-application expo-device expo-localizationReact Native apps
Terminal
PostHog AI
yarn add posthog-react-native @react-native-async-storage/async-storage react-native-device-info react-native-localize
# or
npm i -s posthog-react-native @react-native-async-storage/async-storage react-native-device-info react-native-localizeReact Native Web and macOS
If you're using React Native Web or React Native macOS, do not use the expo-file-system package since the Web and macOS targets aren't supported, use the @react-native-async-storage/async-storage package instead.
Configuration
With the PosthogProvider
The recommended way to set up PostHog for React Native is to use the PostHogProvider. This utilizes the Context API to pass the PostHog client around, and enables autocapture.
To set up PostHogProvider, add it to your App.js or App.ts file:
App.js
PostHog AI
// App.(js|ts)
import { usePostHog, PostHogProvider } from 'posthog-react-native'
...
export function MyApp() {
return (
<PostHogProvider apiKey="<ph_project_token>" options={{
// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
host: 'https://us.i.posthog.com',
}}>
<MyComponent />
</PostHogProvider>
)
}Then you can access PostHog using the usePostHog() hook:
React Native
PostHog AI
const MyComponent = () => {
const posthog = usePostHog()
useEffect(() => {
posthog.capture("event_name")
}, [posthog])
}Without the PosthogProvider
If you prefer not to use the provider, you can initialize PostHog in its own file and import the instance from there:
posthog.ts
PostHog AI
import PostHog from 'posthog-react-native'
export const posthog = new PostHog('<ph_project_token>', {
// usually 'https://us.i.posthog.com' or 'https://eu.i.posthog.com'
host: 'https://us.i.posthog.com'
})Then you can access PostHog by importing your instance:
React Native
PostHog AI
import { posthog } from './posthog'
export function MyApp1() {
useEffect(() => {
posthog.capture('event_name')
}, [])
return <View>Your app code</View>
}You can even use this instance with the PostHogProvider:
React Native
PostHog AI
import { posthog } from './posthog'
export function MyApp() {
return <PostHogProvider client={posthog}>{/* Your app code */}</PostHogProvider>
}2. 2
Configure logs in your PostHog options
Required
Add a logs block to your PostHog initialization. All fields are optional; defaults are tuned for mobile (cellular bandwidth, battery, OS lifecycle).
React Native
PostHog AI
import PostHog from 'posthog-react-native'
const posthog = new PostHog('<ph_project_token>', {
host: 'https://us.i.posthog.com',
logs: {
serviceName: 'my-app', // OTLP service.name – shown in the Logs UI
environment: 'production', // OTLP deployment.environment
serviceVersion: '1.2.3', // OTLP service.version
},
})3. 3
Capture logs
Required
Use posthog.logger for the per-level convenience API, or posthog.captureLog for full control over level, attributes, and trace context.
React Native
PostHog AI
// Per-level convenience methods
posthog.logger.info('checkout completed', { order_id: 'ord_789', amount_cents: 4999 })
posthog.logger.warn('payment retry', { attempt: 2 })
posthog.logger.error('payment failed', { code: 'E001' })
// Lower-level API for custom severity / trace context
posthog.captureLog({
body: 'checkout failed',
level: 'error',
attributes: { order_id: 'ord_789', step: 'auth' },
trace_id: '4bf92f3577b34da6a3ce929d0e0e4736', // optional W3C trace context
span_id: '00f067aa0ba902b7',
})Records are buffered, batched, persisted to disk, and flushed automatically – every 10 seconds, on AppState change (foreground ↔ background), on buffer fill, or on posthog.shutdown(). For an immediate drain, call await posthog.flushLogs().
Each record is automatically tagged with the user's distinct ID, session ID, current screen, app foreground/background state, and active feature flags at the moment of capture.
4. 4
Test your setup
Recommended
1. Capture a test log from your app:
React Native
PostHog AI
posthog.logger.info('hello from RN')
await posthog.flushLogs()2. Open the PostHog Logs UI. 3. Filter by service.name = 'my-app' (or whatever value you set above).
You should see your record arrive within a few seconds.
5. 5
Tune buffering, rate cap, and filtering
Optional
The logs config has knobs for high-volume apps:
React Native
PostHog AI
const posthog = new PostHog('<ph_project_token>', {
logs: {
serviceName: 'my-app',
flushIntervalMs: 5000, // default 10000ms
maxBufferSize: 200, // default 100
rateCap: { maxLogs: 5000, windowMs: 60000 }, // default 500/10s
beforeSend: (record) =>
record.body.includes('secret') ? null : record, // redact or drop
},
})Full configuration reference:
| Field | Default | What it does |
|---|---|---|
| serviceName | 'unknown_service' | OTLP service.name resource attribute |
| serviceVersion | undefined | OTLP service.version resource attribute |
| environment | undefined | OTLP deployment.environment resource attribute |
| resourceAttributes | {} | Extra OTLP resource attributes |
| flushIntervalMs | 10000 | Periodic flush interval in ms |
| maxBufferSize | 100 | Max records held in memory before eviction |
| maxBatchRecordsPerPost | 50 | Max records per outbound POST (halved on 413) |
| rateCap.maxLogs | 500 | Max records per windowMs window |
| rateCap.windowMs | 10000 | Rate-cap window length in ms |
| beforeSend | undefined | Pre-send filter (return null to drop) |
Defaults are tuned for cellular-aware mobile apps (~50 logs/sec ceiling, ~16KB max queue file). Raise rateCap.maxLogs and maxBufferSize for high-volume scenarios.
6. 6
Filtering with beforeSend
Optional
The beforeSend hook runs synchronously before the rate cap, so dropped records don't consume the per-interval budget. Use it for redaction, sampling, or filtering by level:
React Native
PostHog AI
const posthog = new PostHog('<ph_project_token>', {
host: 'https://us.i.posthog.com',
logs: {
serviceName: 'my-app',
beforeSend: (record) => {
// Drop debug logs in production
if (record.level === 'debug') return null
// Redact secrets in the body
return {
...record,
body: record.body.replace(/api_key=\S+/g, 'api_key=[REDACTED]'),
}
},
},
})You can also pass an array of functions to form a chain (evaluated left-to-right). A null return from any link short-circuits and drops the record. A throwing filter never crashes your app: the error is logged and the record is dropped (fail-closed).
8. ## Next steps
Checkpoint
What you can do with your logs
| Action | Description |
|---|---|
| Why you need logs | What logs show you that nothing else does |
| Search logs | Use the search interface to find specific log entries |
| Filter by level | Filter by INFO, WARN, ERROR, etc. |
| Link session replay | Connect logs to users and session replays by passing posthogDistinctId and sessionId |
| Link logs to a person | Surface every log emitted on behalf of a user on their PostHog person profile |
| Logging best practices | Learn what to log, how to structure logs, and patterns that make logs useful in production |
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Search logs - Docs
Filter logs from the filter bar at the top of the logs page. Pick a field, choose an operator, and enter a value. Add as many filters as you need — they're combined with AND.
There are four kinds of fields you can filter on:
- Logs – top-level log properties:
severity_level,trace_id, andspan_id - Message – full-text search over the log body
- Resource attributes – describe where the log came from, like
service.name,host.name, ork8s.container.name - Attributes – custom key-value context attached to individual log events, like
user_id,endpoint, orstatus_code
Filter on resource attributes and attributes
Resource attributes identify the source of a log (the service, host, or container that emitted it). Attributes describe a specific log event. Both come from your OpenTelemetry instrumentation — the richer your structured logging, the more you can filter on.
To filter:
1. Click the filter bar and pick a field. Resource attributes and attributes are grouped separately in the picker. 2. Choose an operator (equals, contains, is set, greater than, etc.). 3. Enter a value.
For example, filter service.name equals checkout-api to scope to one service, then add status_code equals 500 to narrow to failed requests.
Filter by severity, trace ID, and span ID
The Logs group in the filter picker exposes three top-level fields. All three only support equals and not-equals operators.
- severity\_level – filter by log severity using a dropdown. Available values:
trace,debug,info,warn,error,fatal. - trace\_id – filter logs by their OpenTelemetry trace correlation ID. Accepts hex or base64 format trace IDs
- span\_id – filter logs by their OpenTelemetry span ID. Accepts hex or base64, same as
trace_id.
For example, copy a trace_id from a trace URL and paste it into the filter to see every log emitted during that trace.
Full-text search on Message
To search log bodies, pick the Message field from the filter bar. Message supports three operators, each with a negated variant for exclusion:
| Operator | Behavior |
|---|---|
| equals / doesn't equal | Exact match. Case-sensitive. |
| contains / doesn't contain | Substring match. Case-insensitive. The default. |
| matches regex / doesn't match regex | RE2 regex. Case-insensitive. |
Examples
- Contains
failed to connect– matches any log containing that substring, regardless of case. - Equals
Health check OK– matches only logs whose body is exactly that string. - Matches regex
timeout|refused|reset– matches logs mentioning any of those words (useful when you'd otherwise add multiple contains filters). - Doesn't contain
healthcheck– exclude noisy healthcheck lines while keeping everything else.
Tips
- Stack filters to narrow down. Every filter you add is ANDed together — combine a
service.namefilter with a Message contains to scope full-text search to one service. - Start with contains, then tighten. Contains is case-insensitive and forgiving. Switch to equals only when you need an exact match, or regex when you want OR-style matching in a single filter.
- Use regex for alternatives. Instead of adding three contains filters, use one regex like
(timeout|refused|reset). - Structured logs make filtering more powerful. Key-value context like
user_id,endpoint, andstatus_codebecomes an attribute you can filter on directly. See our logging best practices for patterns that make logs easier to query.
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Getting started with Logs - Docs
Use your logging client
PostHog Logs works with any OpenTelemetry client. No PostHog-specific packages required. Use the OTel SDKs you already have, point them at PostHog's HTTP endpoint, and drop in your project token.
On the frontend, our JavaScript web SDK, React Native SDK, iOS SDK, and Android SDK include first-class logging support.
Follow the guides below to set up your logging client:
- 
- 
- 
- 
- 
- 
- 
- 
- 
- 
- 
Send context-rich logs
PostHog ingests logs in the same pattern as OTel's structured logging model: resource attributes, log attributes, and trace context.
Enrich your logs with granular detail and business context for INFO, DEBUG, WARN, and ERROR log levels.
Python
PostHog AI
import logging
# Configure logging to use OpenTelemetry
logging.basicConfig(level=logging.INFO)
logging.getLogger().addHandler(LoggingHandler())
# Use standard Python logging
logger = logging.getLogger("my-app")
logger.info("User action", extra={"userId": "123", "action": "login"})
logger.warning("Deprecated API used", extra={"endpoint": "/old-api"})
logger.error("Database connection failed", extra={"error": "Connection timeout"})Search and analyze your logs
Once your logs are flowing into PostHog, you can:
- Search through logs using full-text searches, multiple search tokens, and negative filters
- Filter by time ranges to find specific events
- Filter on attributes for specific resources or events
- Correlate logs with events from your PostHog analytics
!PostHog Logs search interface!PostHog Logs search interface
Set up alerts
Get notified when your logs match specific conditions. Create alerts to:
- Monitor error spikes — Alert when error log counts exceed a threshold
- Track specific services — Watch for issues in critical services
- Filter by attributes — Set up granular alerts based on log attributes
Configure alerting rules in your project settings to stay on top of issues as they happen.
Use MCP and AI to debug
Connect the PostHog MCP server and your AI agent can query logs directly. Use Cursor, Claude Code, or any MCP-compatible tool.
Your coding agent pulls the relevant logs it needs to debug and build faster without switching workflows.
You can also ask PostHog AI to search and analyze your logs.
!PostHog AI logs!PostHog AI logs
Try out these prompts:
- `Show me error logs from the API service in the last hour`
- `Find all logs related to authentication failures today`
- `Show logs from the payment service around 2pm yesterday`
Integrate your product data
With PostHog, your logs live alongside your Product Analytics, Session Replays, and Error Tracking, so you can go from a log line to a user's session to the flag variant they were on without switching tools.
Session Replay
Log events in PostHog can be connected to the session and user who triggered them. Jump from a log line to a session replay in one click.
!logs and errors!logs and errors
Product Analytics
Turn log patterns into trends, funnels, and retention insights. Know which logged errors actually hurt user retention vs. which are just noise.
!logs and product analytics!logs and product analytics
Error Tracking
Logs with $exception events become issues you can assign, resolve, and alert on. No separate error tracking tool needed.
!logs and session replay!logs and session replay
Use for free
PostHog's Logs is built to be cost-effective by default, with a generous free tier and transparent usage-based pricing. Since we don't charge per seat, more than 90% of companies use PostHog for free.
TL;DR 💸
- No credit card required to start
- First 10 GB of ingested logs per month are free
- Above 10 GB we have usage-based pricing at $0.25/GB with discounts
- All logs are retained 14 days by default, and we also offer 30-day or 90-day retention options for an additional storage charge – see pricing for more details
- Set billing limits to avoid surprise charges
- See our pricing page for more up-to-date details
---
That's it! You're ready to start integrating.
1/7
**Use your logging client** ***Required*****Send context-rich logs** ***Required*****Search and analyze your logs** ***Required*****Set up alerts** ***Recommended*****Use MCP and AI to debug** ***Recommended*****Integrate your product data** ***Recommended*****Use for free** ***Free 10 GB/mo***
Use your logging client
*Required*
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Logs troubleshooting - Docs
This page covers troubleshooting for Logs. For setup, see the installation guide.
Have a question? Ask PostHog AI
Ask PostHog AI
Authentication errors
Problem: Getting 401 Unauthorized errors when sending logs.
Solutions:
- Verify you're using the correct project token from Project Settings
- Check the Authorization header format:
Bearer <ph_project_token> - If using query parameter, verify the format:
?token=<ph_project_token> - Ensure your project token hasn't been rotated or revoked
Connection issues
Problem: Cannot connect to the PostHog Logs endpoint.
Solutions:
- Verify the endpoint URL:
https://us.i.posthog.com/i/v1/logs - Check that your application can make outbound HTTPS requests
- Ensure firewall rules allow outbound connections to PostHog
- For self-hosted instances, verify the endpoint is correct for your deployment
Logs not appearing in PostHog
Problem: Logs are being sent but don't appear in the PostHog interface.
Solutions:
- Verify your project token is correct and associated with the right project
- Check that logs are being sent in the correct OTLP format
- Ensure your project has access to the Logs feature in PostHog
- Check the network tab in your browser/application to verify requests are succeeding (200 status)
Performance issues
Problem: High memory usage or slow log processing.
Solutions:
- Adjust the batch size in your OpenTelemetry configuration
- Use BatchLogRecordProcessor instead of SimpleLogRecordProcessor for better performance
- Consider filtering logs on the client side to reduce volume
- Check for network latency between your application and PostHog
Log format problems
Problem: Logs are being received but not parsed correctly.
Solutions:
- Ensure you're using the standard OTLP log format
- Verify log levels are set correctly (INFO, WARN, ERROR, etc.)
- Check that log attributes are properly structured
- Use the OpenTelemetry logging APIs instead of raw log libraries
Logs not appearing on a person's profile
Problem: Logs are searchable in the Logs view, but the person profile's Logs tab is empty (or missing logs you expected to see).
Solutions:
- Confirm each log record carries the attribute
posthogDistinctId(camelCase, lowercasep) — see Link logs to a person.distinct_id,posthog_distinct_id, anduser_idare not equivalent unless you've explicitly configured one as the custom attribute key. - The value of the attribute must equal one of the person's
distinct_ids exactly — partial or prefixed matches are not picked up. - If your team has customized the attribute key (via the
logs_configendpoint), the person profile's Logs tab shows a hint above the chart indicating which key is being used. Make sure your pipeline emits logs under that exact key. - Date range: the person Logs tab respects the same date range picker as the main Logs view. Expand the range if the logs are older than the default window.
Project token authentication issues
Problem: Confused about which key to use or how to authenticate.
Solutions:
- Use your project token (the same one you use for capturing events)
- Find it in Project Settings
- You can authenticate in two ways:
- Header:
Authorization: Bearer <ph_project_token> - Query param:
?token=<ph_project_token> - Do not use your personal API key or other authentication methods
Self-hosted endpoint issues
Problem: Logs not working with self-hosted PostHog.
Solutions:
- Use your self-hosted instance URL instead of
https://us.i.posthog.com - Verify the logs endpoint is enabled on your self-hosted instance
- Check that the endpoint path is correct:
/logs - Ensure your PostHog version supports the logs feature
Still having issues?
If you're still experiencing problems:
1. Verify your OpenTelemetry client configuration matches the examples in the installation guide 2. Test with a simple log message first before sending complex logs 3. Check the network requests to see the actual HTTP status codes and error messages 4. Contact PostHog support with your specific error messages and configuration details
Community questions
Ask a question
Was this page useful?
HelpfulCould be better
Related skills
FAQ
What does instrument-logs do?
instrument-logs is a Claude Code skill for ai & agent building.
When should I use instrument-logs?
When you need to helps with ai & agent building tasks during AI-assisted development., or when instrument-logs is a claude code skill for ai & agent building.
What are the main capabilities?
instrument-logs; AI & Agent Building; AI-coding skill.