|
| 1 | +**Purpose** |
| 2 | + |
| 3 | +- 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 site with an auto‑generated example log. |
| 4 | + |
| 5 | +**High‑Level Flow** |
| 6 | + |
| 7 | +- Add a typed log structure under `lib/log_struct/log/` (so the docs generator picks it up). |
| 8 | +- Add a configuration toggle in `ConfigStruct::Integrations` and wire it into `Integrations.setup_integrations`. |
| 9 | +- Implement the integration under `lib/log_struct/integrations/…` to produce that log type. |
| 10 | +- Add the dev dependency for the third‑party gem and generate RBIs with Tapioca. |
| 11 | +- Add tests (unit + behavior) under `test/log_struct/integrations` and (if needed) `test/log_struct/log`. |
| 12 | +- Add a short entry in the docs metadata (`site/lib/integration-helpers.ts`). The Integrations page will render it automatically via `AllLogTypes`. |
| 13 | +- Run type export to update the docs’ TypeScript types. |
| 14 | + |
| 15 | +**Conventions** |
| 16 | + |
| 17 | +- Files and naming: |
| 18 | + - Log type: `lib/log_struct/log/<Name>.rb` (class `LogStruct::Log::<Name>`) |
| 19 | + - Integration: `lib/log_struct/integrations/<name>.rb` (module `LogStruct::Integrations::<Name>`) |
| 20 | + - Optional submodules (e.g., `logger.rb`, `log_subscriber.rb`) live below a folder `lib/log_struct/integrations/<name>/`. |
| 21 | + - Tests mirror structure under `test/log_struct/log` and `test/log_struct/integrations`. |
| 22 | +- Type safety: |
| 23 | + - Every Ruby file must be `# typed: strict` and use `T::Sig`. |
| 24 | + - For integration setup signatures: `sig { params(config: LogStruct::Configuration).returns(T.nilable(TrueClass)) }`. |
| 25 | + - Prefer real constants (the gem is a development dependency), but guard runtime with `defined?(::GemModule)` to be safe in user apps. |
| 26 | +- Error handling: |
| 27 | + - Never raise from the integration path; use `LogStruct.handle_exception(error, source: <Source>, context: {integration: :name})`. |
| 28 | +- Logging: |
| 29 | + - Produce a dedicated typed log struct (do NOT emit a generic/plain log). |
| 30 | + - Message naming: concise and stable, e.g., `"ams.render"`, `"ahoy.track"`. |
| 31 | + - Choose the correct `Source` (Rails/App/Job/Storage/…) and a suitable `Event` (`Event::Log` unless a more specific event applies). |
| 32 | + |
| 33 | +**Step‑by‑Step Checklist** |
| 34 | + |
| 35 | +1. Create the typed log struct |
| 36 | + - File: `lib/log_struct/log/<name>.rb` |
| 37 | + - Include: `Interfaces::CommonFields`, `Interfaces::AdditionalDataField`, `SerializeCommon`, `MergeAdditionalDataFields`. |
| 38 | + - Define required fields and defaults (e.g., `message`, specific properties for the integration), and implement `serialize` to add them to the output. |
| 39 | + - Example skeleton: |
| 40 | + |
| 41 | + class LogStruct::Log::Example < T::Struct |
| 42 | + extend T::Sig |
| 43 | + include Interfaces::CommonFields |
| 44 | + include Interfaces::AdditionalDataField |
| 45 | + include SerializeCommon |
| 46 | + include MergeAdditionalDataFields |
| 47 | + |
| 48 | + ExampleEvent = T.type_alias { Event::Log } |
| 49 | + const :source, Source::Rails, default: T.let(Source::Rails, Source::Rails) |
| 50 | + const :event, ExampleEvent, default: T.let(Event::Log, ExampleEvent) |
| 51 | + const :level, Level, default: T.let(Level::Info, Level) |
| 52 | + const :timestamp, Time, factory: -> { Time.now } |
| 53 | + |
| 54 | + const :message, String, default: "example.event" |
| 55 | + const :detail, T.nilable(String), default: nil |
| 56 | + const :additional_data, T::Hash[Symbol, T.untyped], default: {} |
| 57 | + |
| 58 | + sig { override.params(strict: T::Boolean).returns(T::Hash[Symbol, T.untyped]) } |
| 59 | + def serialize(strict = true) |
| 60 | + h = serialize_common(strict) |
| 61 | + merge_additional_data_fields(h) |
| 62 | + h[LOG_KEYS.fetch(:message)] = message |
| 63 | + h[:detail] = detail if detail |
| 64 | + h |
| 65 | + end |
| 66 | + end |
| 67 | + |
| 68 | +2. Register the log type |
| 69 | + - In `lib/log_struct/log.rb`: |
| 70 | + - `require_relative "log/<name>"` |
| 71 | + - Add `T.class_of(LogStruct::Log::<Name>)` to `LogClassType`. |
| 72 | + |
| 73 | +3. Add a config toggle |
| 74 | + - In `lib/log_struct/config_struct/integrations.rb`: |
| 75 | + - `prop :enable_<name>, T::Boolean, default: true` |
| 76 | + - In `lib/log_struct/integrations.rb`: |
| 77 | + - `require_relative "integrations/<name>"` |
| 78 | + - Call `Integrations::<Name>.setup(config)` inside `setup_integrations` behind the toggle. |
| 79 | + |
| 80 | +4. Implement the integration |
| 81 | + - File: `lib/log_struct/integrations/<name>.rb` |
| 82 | + - Guard on presence of the third‑party gem (`return nil unless defined?(::ThirdParty)`), subscribe/hook into events, build your typed log, and log it with `LogStruct.info(log)`. |
| 83 | + - Use `handle_exception` on rescue. |
| 84 | + |
| 85 | +5. Add development dependency + RBIs |
| 86 | + - In `logstruct.gemspec`, add the gem as a development dependency with a supported version: |
| 87 | + - `spec.add_development_dependency "third_party_gem", "~> X.Y"` |
| 88 | + - Run: |
| 89 | + - `bundle install` |
| 90 | + - `bundle exec tapioca gems` (commits RBIs under `sorbet/rbi/gems/...`). |
| 91 | + |
| 92 | +6. Tests |
| 93 | + - Unit/behavior tests under `test/log_struct/integrations/<name>_test.rb`. |
| 94 | + - If the integration exposes a new log struct surface, add a focused test in `test/log_struct/log/<name>_test.rb` when it adds non‑trivial serialization. |
| 95 | + - Prefer real gem objects with small fakes only where necessary; the gem is available as a dev dependency. |
| 96 | + - Assertions should inspect `as_json` and verify: |
| 97 | + - `src`, `evt`, `lvl`, `ts` exist |
| 98 | + - `msg` matches the agreed name |
| 99 | + - Integration‑specific fields are present and correct |
| 100 | + |
| 101 | +7. Docs |
| 102 | + - No custom sections in the Integrations page. |
| 103 | + - The Integrations page lists all `AllLogTypes` with titles/descriptions from `site/lib/integration-helpers.ts`. |
| 104 | + - Add an entry to `getLogTypeInfo` for your new log type (title, concise description, optional `configuration_code: 'integrations_configuration'`). |
| 105 | + - Run the type export to regenerate the docs’ TypeScript assets so example logs render: |
| 106 | + - `ruby scripts/export_typescript_types.rb` |
| 107 | + |
| 108 | +8. Sorbet + CI |
| 109 | + - Run `scripts/typecheck.sh` (must be clean). |
| 110 | + - Run `scripts/test.rb` and `scripts/rails_tests.sh` (must be green). |
| 111 | + - CI will enforce Tapioca verification and coverage threshold (80%+ merged). |
| 112 | + |
| 113 | +**Patterns to Follow (Examples)** |
| 114 | + |
| 115 | +- GoodJob: |
| 116 | + - Log type: `lib/log_struct/log/good_job.rb` |
| 117 | + - Integration: `lib/log_struct/integrations/good_job.rb` (+ `logger.rb`, `log_subscriber.rb`) |
| 118 | + - Toggle: `enable_goodjob` |
| 119 | +- SQL (ActiveRecord): |
| 120 | + - Log type: `lib/log_struct/log/sql.rb` |
| 121 | + - Integration: `lib/log_struct/integrations/active_record.rb` |
| 122 | + - Toggle: `enable_sql_logging` |
| 123 | +- ActiveModelSerializers: |
| 124 | + - Log type: `lib/log_struct/log/active_model_serializers.rb` (msg: "ams.render") |
| 125 | + - Integration: `lib/log_struct/integrations/active_model_serializers.rb` |
| 126 | + - Toggle: `enable_active_model_serializers` |
| 127 | +- Ahoy: |
| 128 | + - Log type: `lib/log_struct/log/ahoy.rb` (msg: "ahoy.track") |
| 129 | + - Integration: `lib/log_struct/integrations/ahoy.rb` |
| 130 | + - Toggle: `enable_ahoy` |
| 131 | + |
| 132 | +**Gotchas** |
| 133 | + |
| 134 | +- Don’t emit generic/plain logs for integrations that deserve a first‑class type. |
| 135 | +- Keep message names short and stable; avoid dumping raw payloads into `msg`. |
| 136 | +- Don’t raise in integration hooks; always call `handle_exception` with the correct `Source`. |
| 137 | +- Docs auto‑generation depends on the log type being included in `LogClassType` and the TypeScript export. |
| 138 | + |
| 139 | +**One‑Time Commands (after adding or changing log types)** |
| 140 | + |
| 141 | +- `bundle install` |
| 142 | +- `bundle exec tapioca gems` |
| 143 | +- `ruby scripts/export_typescript_types.rb` |
| 144 | +- `scripts/typecheck.sh` |
| 145 | +- `scripts/test.rb` and (optionally) `scripts/rails_tests.sh` |
0 commit comments