Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Pyodide #818

Merged
merged 5 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,30 @@ jobs:
env_vars: PYTHON
- run: uv run coverage report --fail-under 100

test-pyodide:
name: test on Pyodide
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.4.30"
enable-cache: true

- uses: actions/setup-node@v4
with:
node-version: "23"

- run: make test-pyodide
env:
UV_PYTHON: ${{ matrix.python-version }}

# https://github.com/marketplace/actions/alls-green#why used for branch protection checks
check:
if: always()
needs: [lint, docs, test, coverage]
needs: [lint, docs, test, coverage, test-pyodide]
runs-on: ubuntu-latest
steps:
- name: Decide whether the needed jobs succeeded or failed
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ testcov: test
@echo "building coverage html"
uv run coverage html --show-contexts

.PHONY: test-pyodide # Check logfire runs with pyodide
test-pyodide:
uv build
cd pyodide_test && npm test

.PHONY: docs # Build the documentation
docs:
uv run mkdocs build
Expand Down
33 changes: 26 additions & 7 deletions logfire/_internal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import functools
import json
import os
import platform
import re
import sys
import time
Expand Down Expand Up @@ -708,6 +709,8 @@ def _initialize(self) -> None:
if self._initialized: # pragma: no cover
return

emscripten = platform.system().lower() == 'emscripten'

with suppress_instrumentation():
otel_resource_attributes: dict[str, Any] = {
ResourceAttributes.SERVICE_NAME: self.service_name,
Expand Down Expand Up @@ -744,7 +747,11 @@ def _initialize(self) -> None:
):
otel_resource_attributes[RESOURCE_ATTRIBUTES_CODE_WORK_DIR] = os.getcwd()

resource = Resource.create(otel_resource_attributes)
if emscripten:
# Resource.create creates a thread pool which fails in Pyodide / Emscripten
resource = Resource(otel_resource_attributes)
else:
resource = Resource.create(otel_resource_attributes)

# Set service instance ID to a random UUID if it hasn't been set already.
# Setting it above would have also mostly worked and allowed overriding via OTEL_RESOURCE_ATTRIBUTES,
Expand Down Expand Up @@ -849,8 +856,11 @@ def check_token():
if show_project_link and validated_credentials is not None:
validated_credentials.print_token_summary()

thread = Thread(target=check_token, name='check_logfire_token')
thread.start()
if emscripten:
check_token()
else:
thread = Thread(target=check_token, name='check_logfire_token')
thread.start()

headers = {'User-Agent': f'logfire/{VERSION}', 'Authorization': self.token}
session = OTLPExporterHttpSession(max_body_size=OTLP_MAX_BODY_SIZE)
Expand All @@ -864,10 +874,19 @@ def check_token():
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
span_exporter = RemovePendingSpansExporter(span_exporter)
schedule_delay_millis = _get_int_from_env(OTEL_BSP_SCHEDULE_DELAY) or 500
add_span_processor(BatchSpanProcessor(span_exporter, schedule_delay_millis=schedule_delay_millis))
if emscripten:
# BatchSpanProcessor uses threads which fail in Pyodide / Emscripten
logfire_processor = SimpleSpanProcessor(span_exporter)
else:
logfire_processor = BatchSpanProcessor(
span_exporter, schedule_delay_millis=schedule_delay_millis
)
add_span_processor(logfire_processor)

if metric_readers is not None:
metric_readers += [
# TODO should we warn here if we have metrics but we're in emscripten?
# I guess we could do some hack to use InMemoryMetricReader and call it after user code has run?
if metric_readers is not None and not emscripten:
metric_readers.append(
PeriodicExportingMetricReader(
QuietMetricExporter(
OTLPMetricExporter(
Expand All @@ -883,7 +902,7 @@ def check_token():
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
)
)
]
)

if processors_with_pending_spans:
pending_multiprocessor = SynchronousMultiSpanProcessor()
Expand Down
49 changes: 49 additions & 0 deletions pyodide_test/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions pyodide_test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "pyodide_test",
"version": "0.0.0",
"main": "test.js",
"scripts": {
"test": "node --experimental-wasm-stack-switching test.mjs"
},
"author": "",
"license": "MIT",
"description": "",
"dependencies": {
"pyodide": "^0.27.2"
}
}
52 changes: 52 additions & 0 deletions pyodide_test/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {opendir} from 'node:fs/promises'
import path from 'path'
import assert from 'assert'
import { loadPyodide } from 'pyodide'


async function runTest() {
const wheelPath = await findWheel(path.join(path.resolve(import.meta.dirname, '..'), 'dist'));
const stdout = []
const stderr = []
const pyodide = await loadPyodide({

stdout: (msg) => {
stdout.push(msg)
},
stderr: (msg) => {
stderr.push(msg)
}
})
await pyodide.loadPackage(['micropip', 'pygments'])
await pyodide.runPythonAsync(`
import sys
import micropip

await micropip.install(['file:${wheelPath}'])
import logfire
logfire.configure(token='unknown', inspect_arguments=False)
logfire.info('hello {name}', name='world')
`)
let out = stdout.join('')
assert.ok(out.includes('hello world'), `stdout did not include message, stdout: "${out}"`)

let err = stderr.join('')
assert.ok(
err.includes(
'UserWarning: Logfire API returned status code 401.'
),
`stderr did not include warning, stderr: "${err}"`
)
}


async function findWheel(dist_dir) {
const dir = await opendir(dist_dir);
for await (const dirent of dir) {
if (dirent.name.endsWith('.whl')) {
return path.join(dist_dir, dirent.name);
}
}
}

runTest().catch(console.error)
12 changes: 1 addition & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -248,17 +248,7 @@ quote-style = "single"
typeCheckingMode = "strict"
reportUnnecessaryTypeIgnoreComment = true
reportMissingTypeStubs = false
exclude = [
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kludex I can revert this if you want, but we're excluding many more things than we're including, so this seemed more sensible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay.

"docs/**/*.py",
"examples/**/*.py",
"site/**/*.py",
".venv",
"venv*",
"**/venv*",
"ignoreme",
"out",
"logfire-api",
]
include = ["logfire", "tests"]
venvPath = "."
venv = ".venv"

Expand Down
Loading