Skip to content

Commit

Permalink
Support Pyodide (#818)
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin authored Jan 27, 2025
1 parent f4d2c63 commit 2bfbddd
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 19 deletions.
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 install && 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: # pragma: no cover
# 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: # pragma: no cover
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: # pragma: no cover
# 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"
}
}
57 changes: 57 additions & 0 deletions pyodide_test/test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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'])
console.log('Running Pyodide test...\n')
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')
sys.stdout.flush()
sys.stderr.flush()
`)
let out = stdout.join('')
let err = stderr.join('')
console.log('stdout:', out)
console.log('stderr:', err)
assert.ok(out.includes('hello world'))

assert.ok(
err.includes(
'UserWarning: Logfire API returned status code 401.'
),
)
console.log('\n\nLogfire Pyodide tests passed 🎉')
}


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 = [
"docs/**/*.py",
"examples/**/*.py",
"site/**/*.py",
".venv",
"venv*",
"**/venv*",
"ignoreme",
"out",
"logfire-api",
]
include = ["logfire", "tests"]
venvPath = "."
venv = ".venv"

Expand Down

0 comments on commit 2bfbddd

Please sign in to comment.