Purpose
- Document how to add a new first‑class integration to LogStruct so it’s consistent, type‑safe, well‑tested, and shows up automatically in the docs docs with an auto‑generated example log.
High‑Level Flow
- Add a typed log structure under
lib/log_struct/log/(so the docs generator picks it up). - Add a configuration toggle in
ConfigStruct::Integrationsand wire it intoIntegrations.setup_integrationsvia the appropriate stage (:non_middlewarefor instrumentation hooks,:middlewarewhen the integration inserts Rack middleware). - Implement the integration under
lib/log_struct/integrations/…to produce that log type. - Add the dev dependency for the third‑party gem and generate RBIs with Tapioca.
- Add tests (unit + behavior) under
test/log_struct/integrationsand (if needed)test/log_struct/log. - Add a short entry in the docs metadata (
docs/lib/integration-helpers.ts). The Integrations page will render it automatically viaAllLogTypes. - Run type export to update the docs’ TypeScript types.
Conventions
- Files and naming:
- Log type:
lib/log_struct/log/<Name>.rb(classLogStruct::Log::<Name>) - Integration:
lib/log_struct/integrations/<name>.rb(moduleLogStruct::Integrations::<Name>) - Optional submodules (e.g.,
logger.rb,log_subscriber.rb) live below a folderlib/log_struct/integrations/<name>/. - Tests mirror structure under
test/log_struct/logandtest/log_struct/integrations.
- Log type:
- Type safety:
- Every Ruby file must be
# typed: strictand useT::Sig. - For integration setup signatures:
sig { params(config: LogStruct::Configuration).returns(T.nilable(TrueClass)) }. - Prefer real constants (the gem is a development dependency), but guard runtime with
defined?(::GemModule)to be safe in user apps.
- Every Ruby file must be
- Error handling:
- Never raise from the integration path; use
LogStruct.handle_exception(error, source: <Source>, context: {integration: :name}).
- Never raise from the integration path; use
- Logging:
- Produce a dedicated typed log struct (do NOT emit a generic/plain log).
- Message naming: concise and stable, e.g.,
"ams.render","ahoy.track". - Choose the correct
Source(Rails/App/Job/Storage/…) and a suitableEvent(Event::Logunless a more specific event applies).
Step‑by‑Step Checklist
-
Create the typed log struct
-
File:
lib/log_struct/log/<name>.rb -
Include:
Interfaces::CommonFields,Interfaces::AdditionalDataField,SerializeCommon,MergeAdditionalDataFields. -
Define required fields and defaults (e.g.,
message, specific properties for the integration), and implementserializeto add them to the output. -
Example skeleton:
class LogStruct::Log::Example < T::Struct extend T::Sig include Interfaces::CommonFields include Interfaces::AdditionalDataField include SerializeCommon include MergeAdditionalDataFields
ExampleEvent = T.type_alias { Event::Log } const :source, Source::Rails, default: T.let(Source::Rails, Source::Rails) const :event, ExampleEvent, default: T.let(Event::Log, ExampleEvent) const :level, Level, default: T.let(Level::Info, Level) const :timestamp, Time, factory: -> { Time.now }
const :message, String, default: "example.event" const :detail, T.nilable(String), default: nil const :additional_data, T::Hash[Symbol, T.untyped], default: {}
sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) } def serialize(strict = true) h = serialize_common(strict) merge_additional_data_fields(h) h[LOG_KEYS.fetch(:message)] = message h[:detail] = detail if detail h end end
-
-
Register the log type
- In
lib/log_struct/log.rb:require_relative "log/<name>"
- In
-
Add a config toggle
- In
lib/log_struct/config_struct/integrations.rb:prop :enable_<name>, T::Boolean, default: true
- In
lib/log_struct/integrations.rb:require_relative "integrations/<name>"- Call
Integrations::<Name>.setup(config)insidesetup_integrations, selecting the stage that matches your integration (:non_middlewarefor subscribers/hooks,:middlewarefor Rack additions). Most integrations belong in the default:non_middlewarestage; only code that mutates the middleware stack should go in:middleware.
- In
-
Implement the integration
- File:
lib/log_struct/integrations/<name>.rb - Guard on presence of the third‑party gem (
return nil unless defined?(::ThirdParty)), subscribe/hook into events, build your typed log, and log it withLogStruct.info(log). - Use
handle_exceptionon rescue.
- File:
-
Add development dependency + RBIs
- In
logstruct.gemspec, add the gem as a development dependency with a supported version:spec.add_development_dependency "third_party_gem", "~> X.Y"
- Run:
bundle installbundle exec tapioca gems(commits RBIs undersorbet/rbi/gems/...).
- In
-
Tests
- Unit/behavior tests under
test/log_struct/integrations/<name>_test.rb. - If the integration exposes a new log struct surface, add a focused test in
test/log_struct/log/<name>_test.rbwhen it adds non‑trivial serialization. - Prefer real gem objects with small fakes only where necessary; the gem is available as a dev dependency.
- Assertions should inspect
as_jsonand verify:src,evt,lvl,tsexistmsgmatches the agreed name- Integration‑specific fields are present and correct
- Unit/behavior tests under
-
Docs
- No custom sections in the Integrations page.
- The Integrations page lists all
AllLogTypeswith titles/descriptions fromdocs/lib/integration-helpers.ts. - Add an entry to
getLogTypeInfofor your new log type (title, concise description, optionalconfiguration_code: 'integrations_configuration'). - Run the type export to regenerate the docs’ TypeScript assets so example logs render:
ruby scripts/generate_structs.rb
-
Sorbet + CI
- Run
scripts/typecheck.sh(must be clean). - Run
scripts/test.rbandscripts/rails_tests.sh(must be green). - CI will enforce Tapioca verification and coverage threshold (80%+ merged).
- Run
Patterns to Follow (Examples)
- GoodJob:
- Log type:
lib/log_struct/log/good_job.rb - Integration:
lib/log_struct/integrations/good_job.rb(+logger.rb,log_subscriber.rb) - Toggle:
enable_goodjob
- Log type:
- SQL (ActiveRecord):
- Log type:
lib/log_struct/log/sql.rb - Integration:
lib/log_struct/integrations/active_record.rb - Toggle:
enable_sql_logging
- Log type:
- ActiveModelSerializers:
- Log type:
lib/log_struct/log/active_model_serializers.rb(msg: "ams.render") - Integration:
lib/log_struct/integrations/active_model_serializers.rb - Toggle:
enable_active_model_serializers
- Log type:
- Ahoy:
- Log type:
lib/log_struct/log/ahoy.rb(msg: "ahoy.track") - Integration:
lib/log_struct/integrations/ahoy.rb - Toggle:
enable_ahoy
- Log type:
Gotchas
- Don’t emit generic/plain logs for integrations that deserve a first‑class type.
- Keep message names short and stable; avoid dumping raw payloads into
msg. - Don’t raise in integration hooks; always call
handle_exceptionwith the correctSource.
One‑Time Commands (after adding or changing log types)
bundle installbundle exec tapioca gemsruby scripts/generate_structs.rbscripts/typecheck.shscripts/test.rband (optionally)scripts/rails_tests.sh