Skip to content

allow LOGFIRE_BASE_URL to accept grpc endpoints #1048

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
134 changes: 96 additions & 38 deletions logfire/_internal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,21 +893,33 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
# try loading credentials (and thus token) from file if a token is not already available
# this takes the lowest priority, behind the token passed to `configure` and the environment variable
if self.token is None:
credentials = LogfireCredentials.load_creds_file(self.data_dir)

# if we still don't have a token, try initializing a new project and writing a new creds file
# note, we only do this if `send_to_logfire` is explicitly `True`, not 'if-token-present'
if self.send_to_logfire is True and credentials is None:
credentials = LogfireCredentials.initialize_project(
logfire_api_url=self.advanced.base_url,
session=requests.Session(),
)
credentials.write_creds_file(self.data_dir)

if credentials is not None:
self.token = credentials.token
self.advanced.base_url = self.advanced.base_url or credentials.logfire_api_url

try:
credentials = LogfireCredentials.load_creds_file(self.data_dir)

# if we still don't have a token, try initializing a new project and writing a new creds file
# note, we only do this if `send_to_logfire` is explicitly `True`, not 'if-token-present'
if self.send_to_logfire is True and credentials is None:
credentials = LogfireCredentials.initialize_project(
logfire_api_url=self.advanced.base_url,
session=requests.Session(),
)
credentials.write_creds_file(self.data_dir)

if credentials is not None:
self.token = credentials.token
self.advanced.base_url = self.advanced.base_url or credentials.logfire_api_url
except LogfireConfigError:
if self.advanced.base_url is not None:
Copy link
Member

Choose a reason for hiding this comment

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

I think we should only allow this if it is a grpc url?

Alternatively, perhaps we could split the logic, and have different code paths depending on whether the endpoint is HTTP/gRPC

# if sending to a custom base url, we allow no
# token (advanced use case, maybe e.g. otel
# collector which has the token configured there)
pass
else:
raise

base_url = None
# NB: grpc (http/2) requires headers to be lowercase
headers = {'user-agent': f'logfire/{VERSION}'}
if self.token is not None:

def check_token():
Expand All @@ -923,14 +935,68 @@ def check_token():
thread.start()

base_url = self.advanced.generate_base_url(self.token)
headers = {'User-Agent': f'logfire/{VERSION}', 'Authorization': self.token}
session = OTLPExporterHttpSession()
session.headers.update(headers)
span_exporter = BodySizeCheckingOTLPSpanExporter(
endpoint=urljoin(base_url, '/v1/traces'),
session=session,
compression=Compression.Gzip,
)
headers['authorization'] = self.token
elif (
self.send_to_logfire is True
and (provided_base_url := self.advanced.base_url) is not None
and provided_base_url.startswith('grpc')
):
# We may not need a token if we are sending to a grpc
# endpoint; it could be an otel collector acting as a proxy
base_url = provided_base_url

if base_url is not None:
if base_url.startswith('grpc://'):
from grpc import Compression as GrpcCompression
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
OTLPLogExporter as GrpcOTLPLogExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
OTLPMetricExporter as GrpcOTLPMetricExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter as GrpcOTLPSpanExporter,
)

span_exporter = GrpcOTLPSpanExporter(
endpoint=base_url, headers=headers, compression=GrpcCompression.Gzip
)
metric_exporter = GrpcOTLPMetricExporter(
endpoint=base_url,
headers=headers,
compression=GrpcCompression.Gzip,
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
)
log_exporter = GrpcOTLPLogExporter(
endpoint=base_url,
headers=headers,
compression=GrpcCompression.Gzip,
)
elif base_url.startswith('http://') or base_url.startswith('https://'):
session = OTLPExporterHttpSession()
session.headers.update(headers)
span_exporter = BodySizeCheckingOTLPSpanExporter(
endpoint=urljoin(base_url, '/v1/traces'),
session=session,
compression=Compression.Gzip,
)
metric_exporter = OTLPMetricExporter(
endpoint=urljoin(base_url, '/v1/metrics'),
headers=headers,
session=session,
compression=Compression.Gzip,
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
)
log_exporter = OTLPLogExporter(
endpoint=urljoin(base_url, '/v1/logs'),
session=session,
compression=Compression.Gzip,
)
else:
raise ValueError(
"Invalid base_url: {base_url}. Must start with 'http://', 'https://', or 'grpc://'."
)

span_exporter = QuietSpanExporter(span_exporter)
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
span_exporter = RemovePendingSpansExporter(span_exporter)
Expand All @@ -946,30 +1012,22 @@ def check_token():

# 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?
# (The point is that PeriodicExportingMetricReader uses threads which fail in Pyodide / Emscripten)
if metric_readers is not None and not emscripten:
metric_readers.append(
PeriodicExportingMetricReader(
QuietMetricExporter(
OTLPMetricExporter(
endpoint=urljoin(base_url, '/v1/metrics'),
headers=headers,
session=session,
compression=Compression.Gzip,
# I'm pretty sure that this line here is redundant,
# and that passing it to the QuietMetricExporter is what matters
# because the PeriodicExportingMetricReader will read it from there.
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
),
metric_exporter,
# NB this could really be retrieved from `metric_exporter` by `QuietMetricExporter`,
# but it is currently a private attribute on `MetricExporter`, we preferred not to reach
# inside the otel SDK details.
#
# Just make sure it always matches.
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
)
)
)

log_exporter = OTLPLogExporter(
endpoint=urljoin(base_url, '/v1/logs'),
session=session,
compression=Compression.Gzip,
)
log_exporter = QuietLogExporter(log_exporter)

if emscripten: # pragma: no cover
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ celery = ["opentelemetry-instrumentation-celery >= 0.42b0"]
django = ["opentelemetry-instrumentation-django >= 0.42b0"]
fastapi = ["opentelemetry-instrumentation-fastapi >= 0.42b0"]
flask = ["opentelemetry-instrumentation-flask >= 0.42b0"]
grpc = ["opentelemetry-exporter-otlp-proto-grpc >= 1.21.0, < 1.33.0"]
httpx = ["opentelemetry-instrumentation-httpx >= 0.42b0"]
starlette = ["opentelemetry-instrumentation-starlette >= 0.42b0"]
sqlalchemy = ["opentelemetry-instrumentation-sqlalchemy >= 0.42b0"]
Expand Down Expand Up @@ -114,6 +115,7 @@ dev = [
"pandas<2.1.2; python_version < '3.9'",
"attrs >= 23.1.0",
"openai >= 1.58.1",
"opentelemetry-exporter-otlp-proto-grpc >= 1.21.0, < 1.33.0",
"opentelemetry-instrumentation-aiohttp-client>=0.42b0",
"opentelemetry-instrumentation-asgi>=0.42b0",
"opentelemetry-instrumentation-wsgi>=0.42b0",
Expand Down
Loading
Loading