diff --git a/README.md b/README.md index 00eaa5f90..a517bd380 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ # Pydantic Logfire β€” Uncomplicated Observability -[![CI](https://github.com/pydantic/logfire/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/pydantic/logfire/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) -[![codecov](https://codecov.io/gh/pydantic/logfire/graph/badge.svg?token=735CNGCGFD)](https://codecov.io/gh/pydantic/logfire) -[![pypi](https://img.shields.io/pypi/v/logfire.svg)](https://pypi.python.org/pypi/logfire) -[![license](https://img.shields.io/github/license/pydantic/logfire.svg)](https://github.com/pydantic/logfire/blob/main/LICENSE) -[![versions](https://img.shields.io/pypi/pyversions/logfire.svg)](https://github.com/pydantic/logfire) +

+ + CI + + + codecov + + + pypi + + + license + + + versions + + + Join Slack + +

From the team behind Pydantic, **Logfire** is an observability platform built on the same belief as our open source library β€” that the most powerful tools can be easy to use. diff --git a/docs/extra/tweaks.css b/docs/extra/tweaks.css index daed915d7..513cc09ed 100644 --- a/docs/extra/tweaks.css +++ b/docs/extra/tweaks.css @@ -79,3 +79,25 @@ li.md-nav__item>a[href^="#logfire.configure("] { .md-search__output em { color: var(--md-primary-fg-color); } + +.md-search__input::-webkit-search-decoration, +.md-search__input::-webkit-search-cancel-button, +.md-search__input::-webkit-search-results-button, +.md-search__input::-webkit-search-results-decoration { + -webkit-appearance:none; +} + +.md-search-result__article { + padding-bottom: .55em; +} + +.ais-SearchBox-form { + display: flex; + flex-direction: row; + gap: 10px; +} + +.md-search-result mark.ais-Highlight-highlighted, +.md-search-result mark.ais-Snippet-highlighted { + color: var(--md-primary-fg-color); +} diff --git a/docs/index.md b/docs/index.md index 3dc8387bd..fc14c9cca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,14 @@ -# Pydantic Logfire +# Getting Started + +## About Logfire From the team behind **Pydantic**, **Logfire** is a new type of observability platform built on the same belief as our open source library β€” that the most powerful tools can be easy to use. -**Logfire** is built on OpenTelemetry, and supports monitoring your application from any language, +**Logfire** is built on OpenTelemetry, and supports monitoring your application from **any language**, with particularly great support for Python! [Read more](why.md). -## Getting Started +## Overview This page is a quick walk-through for setting up a Python app: diff --git a/docs/javascripts/algolia-search.js b/docs/javascripts/algolia-search.js new file mode 100644 index 000000000..be9e2fe76 --- /dev/null +++ b/docs/javascripts/algolia-search.js @@ -0,0 +1,107 @@ +const ALGOLIA_APP_ID = 'KPPUDTIAVX'; +const ALGOLIA_API_KEY = '1fc841595212a2c3afe8c24dd4cb8790'; +const ALGOLIA_INDEX_NAME = 'alt-logfire-docs'; + +const { liteClient: algoliasearch } = window['algoliasearch/lite']; +const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY); + +const search = instantsearch({ + indexName: ALGOLIA_INDEX_NAME, + searchClient, + searchFunction(helper) { + const query = helper.state.query + + if (query && query.length > 1) { + document.querySelector('#hits').hidden = false + document.querySelector('#type-to-start-searching').hidden = true + helper.search(); + } else { + document.querySelector('#hits').hidden = true + document.querySelector('#type-to-start-searching').hidden = false + } + }, +}); + +// create custom widget, to integrate with MkDocs built-in markup +const customSearchBox = instantsearch.connectors.connectSearchBox((renderOptions, isFirstRender) => { + const { query, refine, clear } = renderOptions; + + if (isFirstRender) { + document.querySelector('#searchbox').addEventListener('input', event => { + refine(event.target.value); + }); + + document.querySelector('#searchbox').addEventListener('focus', () => { + document.querySelector('#__search').checked = true; + }); + + document.querySelector('#searchbox-clear').addEventListener('click', () => { + clear(); + }); + + document.querySelector('#searchbox').addEventListener('keydown', (event) => { + // on down arrow, find the first search result and focus it + if (event.key === 'ArrowDown') { + document.querySelector('.md-search-result__link').focus(); + event.preventDefault(); + } + }); + + // for Hits, add keyboard navigation + document.querySelector('#hits').addEventListener('keydown', (event) => { + if (event.key === 'ArrowDown') { + const next = event.target.parentElement.nextElementSibling; + if (next) { + next.querySelector('.md-search-result__link').focus(); + event.preventDefault(); + } + } else if (event.key === 'ArrowUp') { + const prev = event.target.parentElement.previousElementSibling; + if (prev) { + prev.querySelector('.md-search-result__link').focus(); + } else { + document.querySelector('#searchbox').focus(); + } + event.preventDefault(); + } + }) + + document.addEventListener('keydown', (event) => { + // if forward slash is pressed, focus the search box + if (event.key === '/' && event.target.tagName !== 'INPUT') { + document.querySelector('#searchbox').focus(); + event.preventDefault(); + } + }) + } + + + document.querySelector('#type-to-start-searching').hidden = query.length > 1; + document.querySelector('#searchbox').value = query; +}); + +search.addWidgets([ + customSearchBox({}), + + instantsearch.widgets.hits({ + container: '#hits', + cssClasses: { + 'list': 'md-search-result__list', + 'item': 'md-search-result__item' + }, + templates: { + item: (hit, { html, components }) => { + return html` + +
+
+

${components.Highlight({ attribute: 'title', hit })}

+
${components.Snippet({ attribute: 'content', hit })}
+
+
` + }, + }, + }) +]); + +search.start(); diff --git a/docs/javascripts/search-worker.js b/docs/javascripts/search-worker.js deleted file mode 100644 index 8273c5281..000000000 --- a/docs/javascripts/search-worker.js +++ /dev/null @@ -1,69 +0,0 @@ -importScripts('https://cdn.jsdelivr.net/npm/algoliasearch@5.18.0/dist/algoliasearch.umd.min.js') - -const SETUP = 0 -const READY = 1 -const QUERY = 2 -const RESULT = 3 - - -const appID = 'KPPUDTIAVX'; -const apiKey = '1fc841595212a2c3afe8c24dd4cb8790'; -const indexName = 'logfire-docs'; - -const client = algoliasearch.algoliasearch(appID, apiKey); - -self.onmessage = async (event) => { - if (event.data.type === SETUP) { - self.postMessage({ type: READY }); - } else if (event.data.type === QUERY) { - - const query = event.data.data - - if (query === '') { - self.postMessage({ - type: RESULT, data: { - items: [] - } - }); - return - } - - const { results } = await client.search({ - requests: [ - { - indexName, - query, - }, - ], - }); - - const hits = results[0].hits - - // make navigation work with preview deployments - const stripDocsPathName = !(new URL(self.location.href).pathname.startsWith('/docs')); - - const mappedGroupedResults = hits.reduce((acc, hit) => { - if (!acc[hit.pageID]) { - acc[hit.pageID] = [] - } - acc[hit.pageID].push({ - score: 1, - terms: {}, - location: stripDocsPathName ? hit.abs_url.replace('/docs', '') : hit.abs_url, - title: hit.title, - text: hit._highlightResult.content.value, - - }) - return acc - }, {}) - - - - - self.postMessage({ - type: RESULT, data: { - items: Object.values(mappedGroupedResults) - } - }); - } -}; diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 70fb5770e..9c9d4f1a1 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,50 +1,5 @@ {% extends "base.html" %} -{% block config %} - {%- set app = { - "base": base_url, - "features": features, - "translations": {}, - "search": base_url + "/javascripts/search-worker.js" | url - } -%} - - - {%- if config.extra.version -%} - {%- set mike = config.plugins.get("mike") -%} - {%- if not mike or mike.config.version_selector -%} - {%- set _ = app.update({ "version": config.extra.version }) -%} - {%- endif -%} - {%- endif -%} - - - {%- if config.extra.tags -%} - {%- set _ = app.update({ "tags": config.extra.tags }) -%} - {%- endif -%} - - - {%- set translations = app.translations -%} - {%- for key in [ - "clipboard.copy", - "clipboard.copied", - "search.result.placeholder", - "search.result.none", - "search.result.one", - "search.result.other", - "search.result.more.one", - "search.result.more.other", - "search.result.term.missing", - "select.version" - ] -%} - {%- set _ = translations.update({ key: lang.t(key) }) -%} - {%- endfor -%} - - - - -{% endblock %} - {% block content %} {{ super() }} diff --git a/docs/overrides/partials/search.html b/docs/overrides/partials/search.html new file mode 100644 index 000000000..e7dd8394b --- /dev/null +++ b/docs/overrides/partials/search.html @@ -0,0 +1,32 @@ + diff --git a/docs/plugins/algolia.py b/docs/plugins/algolia.py index 223be70fc..9e5b7a618 100644 --- a/docs/plugins/algolia.py +++ b/docs/plugins/algolia.py @@ -1,21 +1,34 @@ from __future__ import annotations as _annotations import os -from typing import Any, cast +from typing import cast from algoliasearch.search_client import SearchClient from bs4 import BeautifulSoup from mkdocs.config import Config from mkdocs.structure.files import Files from mkdocs.structure.pages import Page +from typing_extensions import TypedDict -records: list[dict[str, Any]] = [] -ALGOLIA_INDEX_NAME = 'logfire-docs' + +class AlgoliaRecord(TypedDict): + content: str + pageID: str + abs_url: str + title: str + objectID: str + rank: int + + +records: list[AlgoliaRecord] = [] +ALGOLIA_INDEX_NAME = 'alt-logfire-docs' ALGOLIA_APP_ID = 'KPPUDTIAVX' ALGOLIA_WRITE_API_KEY = os.environ.get('ALGOLIA_WRITE_API_KEY') # Algolia accepts 100k, leaaving some room for other fields MAX_CONTENT_SIZE = 90_000 +HEADING_TAG_NAMES = ['h1', 'h2', 'h3'] + def on_page_content(html: str, page: Page, config: Config, files: Files) -> str: if not ALGOLIA_WRITE_API_KEY: @@ -26,6 +39,13 @@ def on_page_content(html: str, page: Page, config: Config, files: Files) -> str: soup = BeautifulSoup(html, 'html.parser') + # If the page does not start with a heading, add the h1 with the title + # Some examples don't have a heading. or start with h2 + first_element = soup.find() + + if not first_element or not first_element.name or first_element.name not in ['h1', 'h2', 'h3']: + soup.insert(0, BeautifulSoup(f'

{title}

', 'html.parser')) + # Clean up presentational and UI elements for element in soup.find_all(['autoref']): element.decompose() @@ -50,8 +70,10 @@ def on_page_content(html: str, page: Page, config: Config, files: Files) -> str: for extra in soup.find_all('table', attrs={'class': 'highlighttable'}): extra.replace_with(BeautifulSoup(f'
{extra.find("code").get_text()}
', 'html.parser')) - # Find all h1 and h2 headings - headings = soup.find_all(['h1', 'h2']) + headings = soup.find_all(HEADING_TAG_NAMES) + + # Use the rank to put the sections in the beginning higher in the search results + rank = 100 # Process each section for i in range(len(headings)): @@ -62,26 +84,39 @@ def on_page_content(html: str, page: Page, config: Config, files: Files) -> str: # Get content until next heading content: list[str] = [] sibling = current_heading.find_next_sibling() - while sibling and sibling.name not in ['h1', 'h2']: + while sibling and sibling.name not in HEADING_TAG_NAMES: content.append(str(sibling)) sibling = sibling.find_next_sibling() - section_html = ''.join(content) + section_soup = BeautifulSoup(''.join(content), 'html.parser') + section_plain_text = section_soup.get_text(' ', strip=True) # Create anchor URL - anchor_url = f'{page.abs_url}#{heading_id}' if heading_id else page.abs_url + anchor_url = f'{page.abs_url}#{heading_id}' if heading_id else page.abs_url or '' + + record_title = title + + if current_heading.name == 'h2': + record_title = f'{title} - {section_title}' + elif current_heading.name == 'h3': + previous_heading = current_heading.find_previous(['h1', 'h2']) + parent_title = previous_heading.get_text().replace('ΒΆ', '').strip() + record_title = f'{title} - {parent_title} - {section_title}' # Create record for this section records.append( - { - 'content': section_html, - 'pageID': title, - 'abs_url': anchor_url, - 'title': f'{title} - {section_title}', - 'objectID': anchor_url, - } + AlgoliaRecord( + content=section_plain_text, + pageID=title, + abs_url=anchor_url, + title=record_title, + objectID=anchor_url, + rank=rank, + ) ) + rank -= 5 + return html @@ -92,6 +127,15 @@ def on_post_build(config: Config) -> None: client = SearchClient.create(ALGOLIA_APP_ID, ALGOLIA_WRITE_API_KEY) index = client.init_index(ALGOLIA_INDEX_NAME) + index.set_settings( # type: ignore[reportUnknownMemberType] + settings={ + 'searchableAttributes': ['title', 'content'], + 'attributesToSnippet': ['content:40'], + 'customRanking': [ + 'desc(rank)', + ], + } + ) for large_record in list(filter(lambda record: len(record['content']) >= MAX_CONTENT_SIZE, records)): print(f'Content for {large_record["abs_url"]} is too large to be indexed. Skipping...') print(f'Content : {large_record["content"]} characters') diff --git a/docs/plugins/main.py b/docs/plugins/main.py index 6fbde3aee..3a4ecaf9c 100644 --- a/docs/plugins/main.py +++ b/docs/plugins/main.py @@ -92,7 +92,6 @@ def install_logfire(markdown: str, page: Page) -> str: # Split them and strip quotes for each one separately. extras = [arg.strip('\'"') for arg in arguments[1].strip('[]').split(',')] if len(arguments) > 1 else [] package = 'logfire' if not extras else f"'logfire[{','.join(extras)}]'" - extras_arg = ' '.join(f'-E {extra}' for extra in extras) instructions = [ '=== "pip"', ' ```bash', @@ -102,10 +101,6 @@ def install_logfire(markdown: str, page: Page) -> str: ' ```bash', f' uv add {package}', ' ```', - '=== "rye"', - ' ```bash', - f' rye add logfire {extras_arg}', - ' ```', '=== "poetry"', ' ```bash', f' poetry add {package}', diff --git a/mkdocs.yml b/mkdocs.yml index 768d3610b..69fabdc94 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -62,12 +62,16 @@ extra_css: # used for analytics extra_javascript: - "/flarelytics/client.js" + - "https://cdn.jsdelivr.net/npm/algoliasearch@5.20.0/dist/lite/builds/browser.umd.js" + - "https://cdn.jsdelivr.net/npm/instantsearch.js@4.77.3/dist/instantsearch.production.min.js" + - "javascripts/algolia-search.js" nav: - - Logfire: + - Getting Started: - Logfire: index.md - Why Logfire?: why.md - Concepts: concepts.md + - Join Slack: help.md - Onboarding Checklist: - Onboarding Checklist: guides/onboarding-checklist/index.md - Integrate Logfire: guides/onboarding-checklist/integrate.md @@ -148,7 +152,6 @@ nav: - Propagate: reference/api/propagate.md - Exceptions: reference/api/exceptions.md - Pydantic: reference/api/pydantic.md - - Help: help.md - Roadmap: roadmap.md - Release Notes: release-notes.md