Structured Logging for React: Emit Queryable Logs
Structured logging emits logs as JSON objects instead of unformatted text strings. Instead of console.log("User fetched data"), you emit { timestamp: "2026-06-02T14:22:05Z", event: "fetch_user_data", userId: "user-123", duration: 245 }. This allows log aggregation services (Sentry, LogRocket, Datadog) to parse, filter, and query logs by fields. When debugging, you can search: "show all logs where userId equals user-123 and event equals checkout_error"—far more powerful than grepping text files.
Why Structured Logging Matters
Unstructured logs are hard to search and parse:
14:22:05 User fetched data
14:22:08 Request completed
14:22:10 Error: network timeout
Structured logs include rich context:
{
"timestamp": "2026-06-02T14:22:05.123Z",
"level": "info",
"event": "fetch_user_data",
"userId": "user-123",
"sessionId": "sess-456",
"component": "UserProfile",
"duration": 245,
"tags": ["api", "performance"]
}
{
"timestamp": "2026-06-02T14:22:10.456Z",
"level": "error",
"event": "network_timeout",
"userId": "user-123",
"endpoint": "/api/user",
"timeout_ms": 30000,
"retries": 2
}
With structured logs, you can:
- Filter by user, component, or feature.
- Track performance (duration, retries).
- Correlate events across services.
- Generate dashboards and alerts.
- Export logs for analysis.
Setting Up a Logging Library
For React apps, popular structured logging libraries include Pino, Winston, and Bunyan. Pino is lightweight and browser-friendly. Install it:
npm install pino pino-pretty
Or, for a browser-specific logger:
npm install @sentry/browser loglevel
If you are already using Sentry, you can leverage Sentry's logger:
import * as Sentry from "@sentry/react";
Sentry.captureMessage("User checkout started", "info");
Sentry.captureException(new Error("Checkout failed"), {
contexts: {
checkout: {
step: "payment",
cartTotal: 149.99,
itemCount: 3,
},
},
});
For more control, use Pino in a custom logger:
// logger.ts
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL || "info",
transport: {
target: "pino-pretty",
options: {
colorize: true,
ignore: "pid,hostname",
},
},
});
export default logger;
Emitting Structured Logs from React Components
Use the logger throughout your components:
import logger from "./logger";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
const startTime = performance.now();
logger.info({
event: "fetch_user_started",
userId,
component: "UserProfile",
});
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
setUser(data);
const duration = performance.now() - startTime;
logger.info({
event: "fetch_user_success",
userId,
component: "UserProfile",
duration,
dataSize: JSON.stringify(data).length,
});
} catch (error) {
const duration = performance.now() - startTime;
logger.error({
event: "fetch_user_error",
userId,
component: "UserProfile",
error: error.message,
duration,
stack: error.stack,
});
setError(error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
return div>
{loading && p>Loading...</p>}
{error && p style={{ color: "red" }}>Error: {error.message}</p>}
{user && p>Name: {user.name}</p>}
/div>;
}
export default UserProfile;
Every log includes context: userId, component name, and performance metrics. This is far more useful than console.log("Loading user").
Adding Session and User Context
Most logging libraries support middleware or context. Add session and user information automatically:
import logger from "./logger";
import * as Sentry from "@sentry/react";
// In your app initialization
const sessionId = generateUUID();
Sentry.setUser({
id: userId,
email: userEmail,
sessionId, // Custom context
});
// Add context to all logs
logger.childLogger = logger.child({
sessionId,
userId,
environment: process.env.REACT_APP_ENVIRONMENT,
appVersion: process.env.REACT_APP_VERSION,
});
// Use in components
logger.info({
event: "component_mounted",
component: "HomePage",
// sessionId and userId are automatically included
});
Now every log includes the user, session, and version automatically. You can filter: "show all logs where userId = user-123 and environment = production".
Logging Business Events
Beyond technical logs, emit business events:
function Checkout() {
const handleAddToCart = (item) => {
logger.info({
event: "add_to_cart",
itemId: item.id,
itemName: item.name,
price: item.price,
cartTotal: calculateTotal(),
});
};
const handleCheckout = async () => {
logger.info({
event: "checkout_started",
cartItems: items.length,
cartTotal: calculateTotal(),
paymentMethod: selectedPayment,
});
try {
const orderId = await submitOrder();
logger.info({
event: "checkout_success",
orderId,
cartTotal: calculateTotal(),
});
} catch (error) {
logger.error({
event: "checkout_error",
error: error.message,
step: "payment_processing",
});
}
};
return button onClick={handleCheckout}>Place Order</button>;
}
These logs are queryable dashboards: "How many checkouts completed today?" "What is the conversion rate?" "Which payment methods fail most often?"
Filtering Sensitive Data
Always filter PII from logs:
function redactSensitiveData(logObject) {
const redacted = { ...logObject };
if (redacted.email) redacted.email = "[REDACTED]";
if (redacted.password) redacted.password = "[REDACTED]";
if (redacted.creditCard)
redacted.creditCard = redacted.creditCard.slice(-4).padStart(16, "*");
if (redacted.ssn) redacted.ssn = "[REDACTED]";
return redacted;
}
logger.info(redactSensitiveData({
event: "login",
email: "[email protected]",
mfaEnabled: true,
}));
// Logs: { event: "login", email: "[REDACTED]", mfaEnabled: true }
Most logging libraries support a beforeSend hook for this:
logger.onBeforeSend = (logObject) => {
return redactSensitiveData(logObject);
};
Log Levels and Severity
Use appropriate log levels:
- debug: Detailed information for debugging (variable values, function entry/exit).
- info: General informational messages (user actions, feature usage).
- warn: Warning conditions (deprecated API use, performance issues).
- error: Error conditions that do not prevent the app from running (failed request, parsing error).
- fatal: Fatal errors that crash or disable the app.
logger.debug({
event: "render_component",
props: userProps,
state: componentState,
});
logger.info({
event: "user_login",
userId,
});
logger.warn({
event: "deprecated_api_called",
oldFunction: "fetchUserData",
newFunction: "getUserData",
});
logger.error({
event: "api_error",
endpoint: "/api/users",
status: 500,
});
logger.fatal({
event: "app_crash",
reason: "Out of memory",
});
In production, set LOG_LEVEL=info to reduce noise. In development, set LOG_LEVEL=debug for detailed diagnostics.
Querying and Exporting Logs
With Sentry, logs are searchable on the dashboard. Other services (Datadog, LogRocket, ELK Stack) offer more powerful query languages:
# Datadog query
service:react-app
status:error
event:checkout_error
@userId:user-123
env:production
timestamp:[2026-06-02 TO 2026-06-03]
Export logs for analysis or compliance:
# Using Sentry CLI
sentry-cli issues list --project=react-app | sentry-cli events list
Use structured logs to generate reports: "Average page load time by browser," "Error rate by feature," "User retention by cohort."
Key Takeaways
- Structured logging emits JSON logs that are queryable, filterable, and analyzable by machines.
- Include context (userId, sessionId, component, duration) in every log.
- Log business events (checkout_success, add_to_cart) for analytics and monitoring.
- Use appropriate log levels (debug, info, warn, error) to control verbosity.
- Always redact sensitive data (PII, credentials) before logging.
Frequently Asked Questions
Should I use console.log or a structured logger?
Use a structured logger in production. console.log is unstructured and disappears in many environments. A logger sends logs to a backend service for aggregation, searching, and analysis.
Does structured logging affect performance?
Logging is async and non-blocking. A modern logger adds less than 1 ms per log statement. For high-frequency logs (every keystroke), sample them (log 1 in 100) to reduce overhead.
How much data should I include in each log?
Include enough context to understand the event (userId, component, action) but not so much that logs become noise. Aim for 200–500 bytes per log. Avoid logging large objects or arrays unless necessary for debugging.
Can I change log levels dynamically in production?
Yes. Most loggers support changing the level at runtime:
logger.setLevel(process.env.LOG_LEVEL || "info");
Some services (Datadog) allow changing levels on a per-service basis without redeploying.
How do I correlate logs across multiple services (frontend, backend, database)?
Use a correlation ID (a UUID generated for each user request) that all services log:
const correlationId = generateUUID();
logger.info({
event: "request_started",
correlationId,
endpoint: "/api/checkout",
});
Pass the correlation ID to your backend in a header:
fetch("/api/checkout", {
headers: {
"X-Correlation-ID": correlationId,
},
});
Your backend logs include the same correlation ID, allowing you to trace the entire request flow across services.