{"id":25328653,"url":"https://github.com/kit/story_teller","last_synced_at":"2026-02-11T06:32:10.812Z","repository":{"id":75187423,"uuid":"199846017","full_name":"Kit/story_teller","owner":"Kit","description":null,"archived":false,"fork":false,"pushed_at":"2024-11-01T09:38:56.000Z","size":91,"stargazers_count":2,"open_issues_count":2,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-06-28T01:05:51.487Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Kit.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-07-31T11:50:17.000Z","updated_at":"2024-07-23T14:47:28.000Z","dependencies_parsed_at":"2025-02-12T14:59:41.412Z","dependency_job_id":"2a34bb34-a7c1-4ef7-88cf-627602a7f59b","html_url":"https://github.com/Kit/story_teller","commit_stats":null,"previous_names":["kit/story_teller"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Kit/story_teller","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kit%2Fstory_teller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kit%2Fstory_teller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kit%2Fstory_teller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kit%2Fstory_teller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Kit","download_url":"https://codeload.github.com/Kit/story_teller/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Kit%2Fstory_teller/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29328261,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-11T06:13:03.264Z","status":"ssl_error","status_checked_at":"2026-02-11T06:12:55.843Z","response_time":97,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-14T02:56:30.925Z","updated_at":"2026-02-11T06:32:10.795Z","avatar_url":"https://github.com/Kit.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StoryTeller [![CircleCI](https://circleci.com/gh/ConvertKit/story_teller.svg?style=svg\u0026circle-token=e0815a0e82f28c82e3f9a461cb28147d23916059)](https://circleci.com/gh/ConvertKit/story_teller)\n\nIt's just logs. If you want to know what your application is doing, StoryTeller helps understand what's happening in production. Using chapters and stories, you can build complex logs that will tell a story about what is going on in your production environment.\n\n## Core Concepts\n\nEssentially, StoryTeller logs are build around chapters and stories. Stories are conceptually similar to how `Rails.logger` would work. Chapters on the other hand, is a subtle, yet powerful structure that allows you to create context around your stories.\n\nAt the root of everything, we're building key/value logging. And maybe an example would make more sense.\n\n```rb\nclass BooksController \u003c ApplicationController\n  def purchase\n    @purchase = Purchase.create(@integration, params[:orders])\n    if @purchase.errors\n      StoryTeller.tell(\n        errors: @purchase.errors,\n        message: \"Purchase could not be saved.\"\n      )\n    end\n  end\n  around_action only: :purchase do |_, action|\n    StoryTeller.chapter(title: \"Books::Purchase\", subtitle: @integration) do\n      action.call\n    end\n  end\nend\n```\n\nChapters is a way to set some logging context that will execute inside a block. In the example above, the chapter set the context of the book purchase. In the controller action, logs will be created without the need to set a context, because all stories that are executed inside a chapter will inherit all the key/value of all the chapters.\n\n## Documentation\n\nStoryTeller is built around three concepts: `StoryTeller::Book`, `StoryTeller::Chapter` and `StoryTeller::Story`. Together they are the building blocks to create a clear context around your logs.\n\n### Book\nA book is lazily created for each of the thread your ruby program runs. When a chapter or a story is created, it will gather information for the current Book and assign some context, like UUID to the logs.\n\n### Chapter\nChapter is what is created when invoking `StoryTeller.chapter(title:, subtitle:, \u0026block)`. The chapter is a context that is created for all the logs that are going to occur inside the block that is passed. If you have a controller action that instantiates an object which, in turn, does a lot of things that you'd like to log, you should call `StoryTeller.chapter(title:, subtitle:, \u0026block)` and do all the processing inside the block.\n\nThis way, StoryTeller will be able to assign all the logs to a given resource and event. This, in turn will make it possible for you to search for all the logs that happened on event XYZ with resource ABC.\n\n\n### Story\nStories are basically a supercharged version of `Rails.logger.info`. You can pass a hash to it and also use that hash to construct a message.\n\n```rb\nStoryTeller.tell(\n  account_id: @account.id,\n  status: @subscription.status,\n  email: @subscriber.email_address\n  message: \"Found subscriber %{email} with an %{status} subscription.\"\n)\n```\n\nAll the keys that you pass to StoryTeller are going to be indexed and searchable by default. So, as a rule of thumb, you should always store values that you want to interpolate in a string as a seperate entry in the hash because you never know when you'll need to search for those fields.\n\nAlso, if you `tell` a story inside a chapter block, your story will inherit the `event` and the `identifier` from the chapter so those will be indexed and searchable too.\n\n## Set a different level for a story\n\nLevels are a way to specify what kind of story you are writing. Currently, there are 3 levels and you can compose a story to include any of them:\n\n- StoryTeller::STORY_LEVEL\n- StoryTeller::ANALYTIC_LEVEL\n- StoryTeller::ERROR_LEVEL\n\nBy default, every story are set to `StoryTeller::STORY_LEVEL`, except for when an uncaught exception happens inside a chapter. These special story use the `StoryTeller::ERROR_LEVEL`.\n\nThose levels are used in a bitwise field so it's possible to create a story that has any combination of levels.\n\n`StoryTeller.level()` is the API to use to set a different level on a given story. Here's an example of how to write a story that contains both the STORY_LEVEL and the ANALYTIC_LEVEL:\n\n```ruby\nStoryTeller.level(StoryTeller::STORY_LEVEL, StoryTeller::ANALYTIC_LEVEL).tell(\n  my_value: 100,\n  status: \"active\",\n  message: \"This log will be set to both levels\"\n)\n```\n\nBy having different levels, it means you can filter those logs and be able to pipe those to different storage, depending on the level set on them. This is useful for when a chapter is used and different logs are created to generate different type of information.\n\nFor instance, some might be there to make sure the app behaves properly, while other logs is an aggregation of what has happened so it can be graphed in an analytical tool.\n\n## Exception handling\n\nStoryTeller logs any exceptions that occur inside a chapter block. When an exception occurs, it will log it using an internal `StoryTeller.tell` invocation then *reraise the error* so any exception handling above the block can do its thing (bugsnag, or maybe recovering to show a 404, etc).\n\nA `sev` fields mark the severity of a log. In normal logging event, that value will be set to `StoryTeller::Book::INFO_LEVEL`. However, when an exception bubbles to StoryTeller, the chapter will set the severity level of all stories to `StoryTeller::Book::ERROR_LEVEL`. It's often useful to filter by severity level so you can have an overview of all the logs that were involved for a given error.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkit%2Fstory_teller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkit%2Fstory_teller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkit%2Fstory_teller/lists"}