Skip to content

Commit cb57a34

Browse files
committed
improve docs
1 parent 50a0946 commit cb57a34

10 files changed

Lines changed: 753 additions & 176 deletions

File tree

lib/log_struct/enums/event.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class Event < T::Enum
2626
Stream = new(:stream)
2727
Url = new(:url)
2828

29+
# Data generation events
30+
Generate = new(:generate)
31+
2932
# Email events
3033
Delivery = new(:delivery)
3134
Delivered = new(:delivered)

lib/log_struct/log/active_model_serializers.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ class ActiveModelSerializers < T::Struct
2121
include SerializeCommon
2222
include MergeAdditionalDataFields
2323

24-
AMSEvent = T.type_alias { Event::Log }
24+
AMSEvent = T.type_alias { Event::Generate }
2525

2626
# Common fields
2727
const :source, Source::Rails, default: T.let(Source::Rails, Source::Rails)
28-
const :event, AMSEvent, default: T.let(Event::Log, AMSEvent)
28+
const :event, AMSEvent, default: T.let(Event::Generate, AMSEvent)
2929
const :level, Level, default: T.let(Level::Info, Level)
3030
const :timestamp, Time, factory: -> { Time.now }
3131

site/app/docs/integrations/page.tsx

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import { EditPageLink } from '@/components/edit-page-link';
33
import { RubyCodeExample } from '@/components/ruby-code-example';
44
import { HeadingWithAnchor } from '@/components/heading-with-anchor';
55
import { LogGenerator } from '@/lib/log-generation';
6-
import { AllLogTypes } from '@/lib/log-generation/log-types';
6+
import { AllLogTypes, Event } from '@/lib/log-generation/log-types';
77
import { getCodeExample } from '@/lib/codeExamples';
8-
import { getLogTypeInfo, getTitleId } from '@/lib/integration-helpers';
8+
import {
9+
getEventsForLogType,
10+
getLogTypeInfo,
11+
getTitleId,
12+
} from '@/lib/integration-helpers';
13+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
914

1015
// Helper to format logs as JSON strings for display
1116
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -34,7 +39,8 @@ export default function IntegrationsPage() {
3439
const logTypeInfo = getLogTypeInfo(logType);
3540
if (!logTypeInfo) return null; // Skip plain logs, etc.
3641

37-
const { title, description, configuration_code } = logTypeInfo;
42+
const { title, description, configuration_code, preferredEvent } =
43+
logTypeInfo;
3844

3945
return (
4046
<div key={logType} className="mt-10">
@@ -63,15 +69,57 @@ export default function IntegrationsPage() {
6369

6470
{/* Generate a log example for this type */}
6571
<HeadingWithAnchor
66-
id={`${getTitleId(title)}-example`}
72+
id={`${getTitleId(title)}-examples`}
6773
level={2}
6874
className="text-xl font-semibold mt-6 mb-3"
6975
>
70-
Example Log
76+
Example Logs
7177
</HeadingWithAnchor>
72-
<CodeBlock language="json">
73-
{formatLog(logGenerator.generateLog(logType))}
74-
</CodeBlock>
78+
{(() => {
79+
const events = getEventsForLogType(logType);
80+
if (events.length <= 1) {
81+
const only = events[0];
82+
return (
83+
<CodeBlock language="json">
84+
{formatLog(
85+
logGenerator.generateLogWithOptions(logType, {
86+
preferredEvent: preferredEvent ?? only,
87+
}),
88+
)}
89+
</CodeBlock>
90+
);
91+
}
92+
return (
93+
<Tabs defaultValue={String(events[0])}>
94+
<TabsList className="cursor-pointer w-fit flex flex-wrap gap-2">
95+
{events.map((evt) => (
96+
<TabsTrigger
97+
key={String(evt)}
98+
value={String(evt)}
99+
className="cursor-pointer"
100+
>
101+
{String(evt)}
102+
</TabsTrigger>
103+
))}
104+
</TabsList>
105+
{events.map((evt) => (
106+
<TabsContent
107+
key={String(evt)}
108+
value={String(evt)}
109+
className="mt-0.5"
110+
>
111+
<CodeBlock language="json">
112+
{formatLog(
113+
logGenerator.generateLogWithOptions(logType, {
114+
preferredEvent: evt as Event,
115+
}),
116+
)}
117+
</CodeBlock>
118+
</TabsContent>
119+
))}
120+
</Tabs>
121+
);
122+
})()}
75123
</div>
76124
);
77125
})}

site/app/docs/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ export default function DocsLayout({
114114
active={pathname.startsWith('/docs/integrations')}
115115
subHeadings={integrationHeadings}
116116
/>
117+
<DocNavItem
118+
href="/docs/logging"
119+
title="Logging (12‑Factor)"
120+
active={pathname.startsWith('/docs/logging')}
121+
/>
117122
<NestedDocNavItem
118123
href="/docs/filtering-sensitive-data"
119124
title="Filtering Sensitive Data"

site/app/docs/logging/page.tsx

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { HeadingWithAnchor } from '@/components/heading-with-anchor';
2+
import { CodeBlock } from '@/components/code-block';
3+
import { Callout } from '@/components/ui/callout';
4+
import { EditPageLink } from '@/components/edit-page-link';
5+
6+
export default function LoggingDocsPage() {
7+
return (
8+
<div className="space-y-6">
9+
<HeadingWithAnchor id="logging-to-stdout" level={1}>
10+
Logging to STDOUT (12‑Factor)
11+
</HeadingWithAnchor>
12+
<p className="text-lg text-neutral-600 dark:text-neutral-400">
13+
LogStruct embraces{' '}
14+
<a
15+
className="underline"
16+
href="https://12factor.net/logs"
17+
target="_blank"
18+
rel="noreferrer"
19+
>
20+
The Twelve‑Factor App
21+
</a>{' '}
22+
approach to logs: write events as unbuffered lines to{' '}
23+
<code>STDOUT</code> and let the environment aggregate, ship, and store
24+
them. This section explains how Rails logs by default, how LogStruct
25+
integrates, and what to configure for a predictable developer
26+
experience.
27+
</p>
28+
29+
<HeadingWithAnchor id="rails-defaults" level={2}>
30+
Rails Defaults vs. LogStruct
31+
</HeadingWithAnchor>
32+
<ul className="list-disc list-inside space-y-2 text-neutral-600 dark:text-neutral-400">
33+
<li>
34+
<b>Rails (development):</b> writes to <code>log/development.log</code>{' '}
35+
by default; the server console shows Puma boot lines, not application
36+
logs.
37+
</li>
38+
<li>
39+
<b>Rails (test):</b> most test runners capture logs; default logger
40+
often writes to a file.
41+
</li>
42+
<li>
43+
<b>Rails (production):</b> many deploy targets set{' '}
44+
<code>RAILS_LOG_TO_STDOUT=1</code>, so logs go to STDOUT.
45+
</li>
46+
<li>
47+
<b>LogStruct:</b> when enabled, replaces the logger with
48+
SemanticLogger and emits JSON to STDOUT in test/production by default.
49+
In development, you can opt‑in to the same JSON to avoid surprises.
50+
</li>
51+
</ul>
52+
53+
<HeadingWithAnchor id="dev-parity" level={2}>
54+
Make Development Match Test/Production
55+
</HeadingWithAnchor>
56+
<p className="text-neutral-600 dark:text-neutral-400">
57+
Opt‑in locally so development behaves like test/production:
58+
</p>
59+
<CodeBlock language="bash">
60+
{`# One-off
61+
LOGSTRUCT_ENABLED=true RAILS_LOG_TO_STDOUT=1 rails s
62+
63+
# Or set in your shell env for the session
64+
export LOGSTRUCT_ENABLED=true
65+
export RAILS_LOG_TO_STDOUT=1
66+
rails s`}
67+
</CodeBlock>
68+
<Callout className="mt-4">
69+
You can also force STDOUT + debug in development via code (useful for
70+
teams):
71+
</Callout>
72+
<CodeBlock language="ruby">
73+
{`# config/environments/development.rb (or in your application template)
74+
config.log_level = :debug
75+
logger = ActiveSupport::Logger.new($stdout)
76+
logger.formatter = config.log_formatter
77+
config.logger = ActiveSupport::TaggedLogging.new(logger)`}
78+
</CodeBlock>
79+
80+
<HeadingWithAnchor id="dotenv-rails" level={2}>
81+
Dotenv‑Rails and Early Boot Logs
82+
</HeadingWithAnchor>
83+
<p className="text-neutral-600 dark:text-neutral-400">
84+
Some libraries (e.g., <code>dotenv-rails</code>) log very early during
85+
boot. LogStruct subscribes to those notifications immediately and
86+
buffers structured logs. After your initializers run, LogStruct decides
87+
which set to emit:
88+
</p>
89+
<ul className="list-disc list-inside space-y-2 text-neutral-600 dark:text-neutral-400">
90+
<li>
91+
<b>Enabled:</b> Emit structured JSON logs (for example, a dotenv{' '}
92+
<em>update</em> event) and suppress original replay.
93+
</li>
94+
<li>
95+
<b>Disabled:</b> Emit original <code>[dotenv]</code> lines and discard
96+
the structured buffer.
97+
</li>
98+
</ul>
99+
<Callout type="warning" className="mt-2">
100+
When you run a <code>rails runner</code> inside your test suite, it
101+
inherits <code>RAILS_ENV=test</code>. Dotenv may have already loaded{' '}
102+
<code>.env.test</code>, so nested runs might only show a single “Loaded
103+
…” line (no “Set …” update).
104+
</Callout>
105+
106+
<HeadingWithAnchor id="production-recommendations" level={2}>
107+
Production Recommendations
108+
</HeadingWithAnchor>
109+
<ul className="list-disc list-inside space-y-2 text-neutral-600 dark:text-neutral-400">
110+
<li>
111+
Ensure <code>RAILS_LOG_TO_STDOUT=1</code> (many platforms set this by
112+
default).
113+
</li>
114+
<li>
115+
Keep LogStruct enabled in production (default) to emit structured JSON
116+
for all integrations.
117+
</li>
118+
<li>
119+
Ship logs to your log aggregation system (e.g., CloudWatch, ELK,
120+
Datadog) as line‑delimited JSON.
121+
</li>
122+
</ul>
123+
124+
<HeadingWithAnchor id="quick-diagnostics" level={2}>
125+
Quick Diagnostics
126+
</HeadingWithAnchor>
127+
<CodeBlock language="bash">
128+
{`# Verify structured boot logs (dotenv) in test env
129+
LOGSTRUCT_ENABLED=true RAILS_LOG_TO_STDOUT=1 DISABLE_SPRING=1 RAILS_ENV=test \
130+
rails runner 'puts LogStruct.enabled?'
131+
132+
# Verify original dotenv lines when disabled
133+
LOGSTRUCT_ENABLED=false RAILS_LOG_TO_STDOUT=1 DISABLE_SPRING=1 RAILS_ENV=development \
134+
rails runner 'puts LogStruct.enabled?'`}
135+
</CodeBlock>
136+
137+
<EditPageLink />
138+
</div>
139+
);
140+
}

site/lib/integration-helpers.ts

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { LogType } from '@/lib/log-generation/log-types';
1+
import { Event, LogType } from '@/lib/log-generation/log-types';
22

33
// Generate information for each log type (extracted from integrations/page.tsx)
44
export function getLogTypeInfo(logType: LogType): {
55
title: string;
66
description: string;
77
configuration_code?: string;
8+
preferredEvent?: Event;
89
} | null {
910
switch (logType) {
1011
case LogType.AHOY:
@@ -91,6 +92,14 @@ export function getLogTypeInfo(logType: LogType): {
9192
'Captures ActiveRecord SQL queries with duration, operation type, table names, and optional bind parameters, with smart filtering of noisy queries.',
9293
};
9394

95+
case LogType.DOTENV:
96+
return {
97+
title: 'Dotenv',
98+
description:
99+
'Converts dotenv-rails boot messages (load/update/save/restore) into structured JSON. Early boot events are buffered and emitted once configuration is loaded.',
100+
preferredEvent: Event.UPDATE,
101+
};
102+
94103
case LogType.ERROR:
95104
return {
96105
title: 'Error Handling',
@@ -103,7 +112,56 @@ export function getLogTypeInfo(logType: LogType): {
103112
return null;
104113

105114
default:
106-
return null;
115+
// Ensure we have an exhaustive switch statement
116+
logType satisfies never;
117+
throw new Error(`Unhandled log type: ${logType}`);
118+
}
119+
}
120+
121+
// Events to showcase for each log type (used for example tabs)
122+
export function getEventsForLogType(logType: LogType): Event[] {
123+
switch (logType) {
124+
case LogType.REQUEST:
125+
return [Event.REQUEST];
126+
case LogType.ACTIVEJOB:
127+
// ActiveJob struct covers enqueue/schedule/start/finish
128+
return [Event.ENQUEUE, Event.SCHEDULE, Event.START, Event.FINISH];
129+
case LogType.PLAIN:
130+
return [Event.LOG];
131+
case LogType.ERROR:
132+
return [Event.ERROR];
133+
case LogType.SECURITY:
134+
return [Event.BLOCKED_HOST, Event.CSRF_VIOLATION, Event.IP_SPOOF];
135+
case LogType.SHRINE:
136+
return [Event.UPLOAD, Event.DOWNLOAD, Event.DELETE];
137+
case LogType.SIDEKIQ:
138+
return [Event.LOG];
139+
case LogType.ACTIVESTORAGE:
140+
return [
141+
Event.UPLOAD,
142+
Event.DOWNLOAD,
143+
Event.DELETE,
144+
Event.METADATA,
145+
Event.EXIST,
146+
Event.URL,
147+
];
148+
case LogType.ACTIONMAILER:
149+
return [Event.DELIVERY, Event.DELIVERED];
150+
case LogType.CARRIERWAVE:
151+
return [Event.UPLOAD, Event.DELETE];
152+
case LogType.GOODJOB:
153+
return [Event.ENQUEUE, Event.START, Event.FINISH, Event.ERROR, Event.LOG];
154+
case LogType.SQL:
155+
return [Event.DATABASE];
156+
case LogType.ACTIVEMODELSERIALIZERS:
157+
return [Event.LOG];
158+
case LogType.AHOY:
159+
return [Event.LOG];
160+
case LogType.DOTENV:
161+
return [Event.UPDATE, Event.LOAD, Event.SAVE, Event.RESTORE];
162+
default:
163+
logType satisfies never;
164+
return [Event.LOG];
107165
}
108166
}
109167

@@ -113,29 +171,3 @@ export function getTitleId(title: string): string {
113171
.replace(/\s+/g, '-')
114172
.replace(/[^a-z0-9-]/g, '');
115173
}
116-
117-
// Additional integrations not represented as LogType entries but supported by the gem.
118-
// Centralize their titles/descriptions here to keep docs consistent.
119-
export type ExtraIntegration = {
120-
id: string; // stable anchor/id
121-
title: string;
122-
description: string;
123-
configuration_code?: string;
124-
};
125-
126-
export const AdditionalIntegrations: ExtraIntegration[] = [
127-
{
128-
id: 'ahoy',
129-
title: 'Ahoy',
130-
description:
131-
'When ahoy_matey is present, LogStruct emits lightweight structured logs for analytics events tracked via Ahoy::Tracker#track. Toggle with config.integrations.enable_ahoy.',
132-
configuration_code: 'integrations_configuration',
133-
},
134-
{
135-
id: 'active-model-serializers',
136-
title: 'ActiveModelSerializers',
137-
description:
138-
'If ActiveModelSerializers is present, LogStruct subscribes to *.active_model_serializers notifications and logs serializer name, adapter, resource class, and render duration. Toggle with config.integrations.enable_active_model_serializers.',
139-
configuration_code: 'integrations_configuration',
140-
},
141-
];

0 commit comments

Comments
 (0)