Skip to content

Commit d3f93df

Browse files
committed
allow LOGFIRE_BASE_URL to accept grpc endpoints
1 parent a616cab commit d3f93df

File tree

3 files changed

+3613
-3395
lines changed

3 files changed

+3613
-3395
lines changed

logfire/_internal/config.py

Lines changed: 93 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -893,21 +893,33 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
893893
# try loading credentials (and thus token) from file if a token is not already available
894894
# this takes the lowest priority, behind the token passed to `configure` and the environment variable
895895
if self.token is None:
896-
credentials = LogfireCredentials.load_creds_file(self.data_dir)
897-
898-
# if we still don't have a token, try initializing a new project and writing a new creds file
899-
# note, we only do this if `send_to_logfire` is explicitly `True`, not 'if-token-present'
900-
if self.send_to_logfire is True and credentials is None:
901-
credentials = LogfireCredentials.initialize_project(
902-
logfire_api_url=self.advanced.base_url,
903-
session=requests.Session(),
904-
)
905-
credentials.write_creds_file(self.data_dir)
906-
907-
if credentials is not None:
908-
self.token = credentials.token
909-
self.advanced.base_url = self.advanced.base_url or credentials.logfire_api_url
910-
896+
try:
897+
credentials = LogfireCredentials.load_creds_file(self.data_dir)
898+
899+
# if we still don't have a token, try initializing a new project and writing a new creds file
900+
# note, we only do this if `send_to_logfire` is explicitly `True`, not 'if-token-present'
901+
if self.send_to_logfire is True and credentials is None:
902+
credentials = LogfireCredentials.initialize_project(
903+
logfire_api_url=self.advanced.base_url,
904+
session=requests.Session(),
905+
)
906+
credentials.write_creds_file(self.data_dir)
907+
908+
if credentials is not None:
909+
self.token = credentials.token
910+
self.advanced.base_url = self.advanced.base_url or credentials.logfire_api_url
911+
except LogfireConfigError:
912+
if self.advanced.base_url is not None:
913+
# if sending to a custom base url, we allow no
914+
# token (advanced use case, maybe e.g. otel
915+
# collector which has the token configured there)
916+
pass
917+
else:
918+
raise
919+
920+
base_url = None
921+
# NB: grpc (http/2) requires headers to be lowercase
922+
headers = {'user-agent': f'logfire/{VERSION}'}
911923
if self.token is not None:
912924

913925
def check_token():
@@ -923,14 +935,70 @@ def check_token():
923935
thread.start()
924936

925937
base_url = self.advanced.generate_base_url(self.token)
926-
headers = {'User-Agent': f'logfire/{VERSION}', 'Authorization': self.token}
927-
session = OTLPExporterHttpSession()
928-
session.headers.update(headers)
929-
span_exporter = BodySizeCheckingOTLPSpanExporter(
930-
endpoint=urljoin(base_url, '/v1/traces'),
931-
session=session,
932-
compression=Compression.Gzip,
933-
)
938+
headers['authorization'] = self.token
939+
elif self.send_to_logfire is True and self.advanced.base_url is not None:
940+
# We may not need a token if we are sending to a custom
941+
# base URL
942+
base_url = self.advanced.base_url
943+
944+
if base_url is not None:
945+
if base_url.startswith('grpc://'):
946+
from grpc import Compression as GrpcCompression
947+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
948+
OTLPLogExporter as GrpcOTLPLogExporter,
949+
)
950+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
951+
OTLPMetricExporter as GrpcOTLPMetricExporter,
952+
)
953+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
954+
OTLPSpanExporter as GrpcOTLPSpanExporter,
955+
)
956+
957+
span_exporter = GrpcOTLPSpanExporter(
958+
endpoint=base_url, headers=headers, compression=GrpcCompression.Gzip
959+
)
960+
metric_exporter = GrpcOTLPMetricExporter(
961+
endpoint=base_url,
962+
headers=headers,
963+
compression=GrpcCompression.Gzip,
964+
# I'm pretty sure that this line here is redundant,
965+
# and that passing it to the QuietMetricExporter is what matters
966+
# because the PeriodicExportingMetricReader will read it from there.
967+
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
968+
)
969+
log_exporter = GrpcOTLPLogExporter(
970+
endpoint=base_url,
971+
headers=headers,
972+
compression=GrpcCompression.Gzip,
973+
)
974+
elif base_url.startswith('http://') or base_url.startswith('https://'):
975+
session = OTLPExporterHttpSession()
976+
session.headers.update(headers)
977+
span_exporter = BodySizeCheckingOTLPSpanExporter(
978+
endpoint=urljoin(base_url, '/v1/traces'),
979+
session=session,
980+
compression=Compression.Gzip,
981+
)
982+
metric_exporter = OTLPMetricExporter(
983+
endpoint=urljoin(base_url, '/v1/metrics'),
984+
headers=headers,
985+
session=session,
986+
compression=Compression.Gzip,
987+
# I'm pretty sure that this line here is redundant,
988+
# and that passing it to the QuietMetricExporter is what matters
989+
# because the PeriodicExportingMetricReader will read it from there.
990+
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
991+
)
992+
log_exporter = OTLPLogExporter(
993+
endpoint=urljoin(base_url, '/v1/logs'),
994+
session=session,
995+
compression=Compression.Gzip,
996+
)
997+
else:
998+
raise ValueError(
999+
"Invalid base_url: {base_url}. Must start with 'http://', 'https://', or 'grpc://'."
1000+
)
1001+
9341002
span_exporter = QuietSpanExporter(span_exporter)
9351003
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
9361004
span_exporter = RemovePendingSpansExporter(span_exporter)
@@ -946,30 +1014,17 @@ def check_token():
9461014

9471015
# TODO should we warn here if we have metrics but we're in emscripten?
9481016
# I guess we could do some hack to use InMemoryMetricReader and call it after user code has run?
1017+
# (The point is that PeriodicExportingMetricReader uses threads which fail in Pyodide / Emscripten)
9491018
if metric_readers is not None and not emscripten:
9501019
metric_readers.append(
9511020
PeriodicExportingMetricReader(
9521021
QuietMetricExporter(
953-
OTLPMetricExporter(
954-
endpoint=urljoin(base_url, '/v1/metrics'),
955-
headers=headers,
956-
session=session,
957-
compression=Compression.Gzip,
958-
# I'm pretty sure that this line here is redundant,
959-
# and that passing it to the QuietMetricExporter is what matters
960-
# because the PeriodicExportingMetricReader will read it from there.
961-
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
962-
),
1022+
metric_exporter,
9631023
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
9641024
)
9651025
)
9661026
)
9671027

968-
log_exporter = OTLPLogExporter(
969-
endpoint=urljoin(base_url, '/v1/logs'),
970-
session=session,
971-
compression=Compression.Gzip,
972-
)
9731028
log_exporter = QuietLogExporter(log_exporter)
9741029

9751030
if emscripten: # pragma: no cover

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ celery = ["opentelemetry-instrumentation-celery >= 0.42b0"]
6565
django = ["opentelemetry-instrumentation-django >= 0.42b0"]
6666
fastapi = ["opentelemetry-instrumentation-fastapi >= 0.42b0"]
6767
flask = ["opentelemetry-instrumentation-flask >= 0.42b0"]
68+
grpc = ["opentelemetry-exporter-otlp-proto-grpc >= 1.21.0, < 1.33.0"]
6869
httpx = ["opentelemetry-instrumentation-httpx >= 0.42b0"]
6970
starlette = ["opentelemetry-instrumentation-starlette >= 0.42b0"]
7071
sqlalchemy = ["opentelemetry-instrumentation-sqlalchemy >= 0.42b0"]
@@ -114,6 +115,7 @@ dev = [
114115
"pandas<2.1.2; python_version < '3.9'",
115116
"attrs >= 23.1.0",
116117
"openai >= 1.58.1",
118+
"opentelemetry-exporter-otlp-proto-grpc >= 1.21.0, < 1.33.0",
117119
"opentelemetry-instrumentation-aiohttp-client>=0.42b0",
118120
"opentelemetry-instrumentation-asgi>=0.42b0",
119121
"opentelemetry-instrumentation-wsgi>=0.42b0",

0 commit comments

Comments
 (0)