Skip to content

Commit 7102d71

Browse files
committed
allow LOGFIRE_BASE_URL to accept grpc endpoints
1 parent 9b04e2c commit 7102d71

File tree

2 files changed

+97
-35
lines changed

2 files changed

+97
-35
lines changed

logfire/_internal/config.py

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -901,18 +901,31 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
901901
# try loading credentials (and thus token) from file if a token is not already available
902902
# this takes the lowest priority, behind the token passed to `configure` and the environment variable
903903
if not self.token:
904-
credentials = LogfireCredentials.load_creds_file(self.data_dir)
905-
906-
# if we still don't have a token, try initializing a new project and writing a new creds file
907-
# note, we only do this if `send_to_logfire` is explicitly `True`, not 'if-token-present'
908-
if self.send_to_logfire is True and credentials is None:
909-
client = LogfireClient.from_url(self.advanced.base_url)
910-
credentials = LogfireCredentials.initialize_project(client=client)
911-
credentials.write_creds_file(self.data_dir)
912-
913-
if credentials is not None:
914-
self.token = credentials.token
915-
self.advanced.base_url = self.advanced.base_url or credentials.logfire_api_url
904+
try:
905+
credentials = LogfireCredentials.load_creds_file(self.data_dir)
906+
907+
# if we still don't have a token, try initializing a new project and writing a new creds file
908+
# note, we only do this if `send_to_logfire` is explicitly `True`, not 'if-token-present'
909+
if self.send_to_logfire is True and credentials is None:
910+
client = LogfireClient.from_url(self.advanced.base_url)
911+
credentials = LogfireCredentials.initialize_project(client=client)
912+
credentials.write_creds_file(self.data_dir)
913+
914+
if credentials is not None:
915+
self.token = credentials.token
916+
self.advanced.base_url = self.advanced.base_url or credentials.logfire_api_url
917+
except LogfireConfigError:
918+
if self.advanced.base_url is not None and self.advanced.base_url.startswith('grpc://'):
919+
# if sending to a custom GRPC endpoint, we allow no
920+
# token (advanced use case, maybe e.g. otel
921+
# collector which has the token configured there)
922+
pass
923+
else:
924+
raise
925+
926+
base_url = None
927+
# NB: grpc (http/2) requires headers to be lowercase
928+
headers = {'user-agent': f'logfire/{VERSION}'}
916929

917930
if self.token:
918931

@@ -929,14 +942,74 @@ def check_token():
929942
thread.start()
930943

931944
base_url = self.advanced.generate_base_url(self.token)
932-
headers = {'User-Agent': f'logfire/{VERSION}', 'Authorization': self.token}
933-
session = OTLPExporterHttpSession()
934-
session.headers.update(headers)
935-
span_exporter = BodySizeCheckingOTLPSpanExporter(
936-
endpoint=urljoin(base_url, '/v1/traces'),
937-
session=session,
938-
compression=Compression.Gzip,
939-
)
945+
headers['authorization'] = self.token
946+
elif (
947+
self.send_to_logfire is True
948+
and (provided_base_url := self.advanced.base_url) is not None
949+
and provided_base_url.startswith('grpc')
950+
):
951+
# We may not need a token if we are sending to a grpc
952+
# endpoint; it could be an otel collector acting as a proxy
953+
base_url = provided_base_url
954+
955+
if base_url is not None:
956+
if base_url.startswith('grpc://'):
957+
from grpc import Compression as GrpcCompression
958+
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
959+
OTLPLogExporter as GrpcOTLPLogExporter,
960+
)
961+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
962+
OTLPMetricExporter as GrpcOTLPMetricExporter,
963+
)
964+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
965+
OTLPSpanExporter as GrpcOTLPSpanExporter,
966+
)
967+
968+
span_exporter = GrpcOTLPSpanExporter(
969+
endpoint=base_url, headers=headers, compression=GrpcCompression.Gzip
970+
)
971+
metric_exporter = GrpcOTLPMetricExporter(
972+
endpoint=base_url,
973+
headers=headers,
974+
compression=GrpcCompression.Gzip,
975+
# I'm pretty sure that this line here is redundant,
976+
# and that passing it to the QuietMetricExporter is what matters
977+
# because the PeriodicExportingMetricReader will read it from there.
978+
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
979+
)
980+
log_exporter = GrpcOTLPLogExporter(
981+
endpoint=base_url,
982+
headers=headers,
983+
compression=GrpcCompression.Gzip,
984+
)
985+
elif base_url.startswith('http://') or base_url.startswith('https://'):
986+
session = OTLPExporterHttpSession()
987+
session.headers.update(headers)
988+
span_exporter = BodySizeCheckingOTLPSpanExporter(
989+
endpoint=urljoin(base_url, '/v1/traces'),
990+
session=session,
991+
compression=Compression.Gzip,
992+
)
993+
metric_exporter = OTLPMetricExporter(
994+
endpoint=urljoin(base_url, '/v1/metrics'),
995+
headers=headers,
996+
session=session,
997+
compression=Compression.Gzip,
998+
# I'm pretty sure that this line here is redundant,
999+
# and that passing it to the QuietMetricExporter is what matters
1000+
# because the PeriodicExportingMetricReader will read it from there.
1001+
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
1002+
)
1003+
log_exporter = OTLPLogExporter(
1004+
endpoint=urljoin(base_url, '/v1/logs'),
1005+
session=session,
1006+
compression=Compression.Gzip,
1007+
)
1008+
else:
1009+
raise ValueError(
1010+
"Invalid base_url: {base_url}. Must start with 'http://', 'https://', or 'grpc://'."
1011+
)
1012+
9401013
span_exporter = QuietSpanExporter(span_exporter)
9411014
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
9421015
span_exporter = RemovePendingSpansExporter(span_exporter)
@@ -949,30 +1022,17 @@ def check_token():
9491022

9501023
# TODO should we warn here if we have metrics but we're in emscripten?
9511024
# I guess we could do some hack to use InMemoryMetricReader and call it after user code has run?
1025+
# (The point is that PeriodicExportingMetricReader uses threads which fail in Pyodide / Emscripten)
9521026
if metric_readers is not None and not emscripten:
9531027
metric_readers.append(
9541028
PeriodicExportingMetricReader(
9551029
QuietMetricExporter(
956-
OTLPMetricExporter(
957-
endpoint=urljoin(base_url, '/v1/metrics'),
958-
headers=headers,
959-
session=session,
960-
compression=Compression.Gzip,
961-
# I'm pretty sure that this line here is redundant,
962-
# and that passing it to the QuietMetricExporter is what matters
963-
# because the PeriodicExportingMetricReader will read it from there.
964-
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
965-
),
1030+
metric_exporter,
9661031
preferred_temporality=METRICS_PREFERRED_TEMPORALITY,
9671032
)
9681033
)
9691034
)
9701035

971-
log_exporter = OTLPLogExporter(
972-
endpoint=urljoin(base_url, '/v1/logs'),
973-
session=session,
974-
compression=Compression.Gzip,
975-
)
9761036
log_exporter = QuietLogExporter(log_exporter)
9771037

9781038
if emscripten: # pragma: no cover

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ celery = ["opentelemetry-instrumentation-celery >= 0.42b0"]
6666
django = ["opentelemetry-instrumentation-django >= 0.42b0", "opentelemetry-instrumentation-asgi >= 0.42b0"]
6767
fastapi = ["opentelemetry-instrumentation-fastapi >= 0.42b0"]
6868
flask = ["opentelemetry-instrumentation-flask >= 0.42b0"]
69+
grpc = ["opentelemetry-exporter-otlp-proto-grpc >= 1.21.0, < 1.33.0"]
6970
httpx = ["opentelemetry-instrumentation-httpx >= 0.42b0"]
7071
starlette = ["opentelemetry-instrumentation-starlette >= 0.42b0"]
7172
sqlalchemy = ["opentelemetry-instrumentation-sqlalchemy >= 0.42b0"]
@@ -113,6 +114,7 @@ dev = [
113114
"pandas>=2.1.2",
114115
"attrs >= 23.1.0",
115116
"openai >= 1.58.1",
117+
"opentelemetry-exporter-otlp-proto-grpc >= 1.21.0, < 1.33.0",
116118
"opentelemetry-instrumentation-aiohttp-client>=0.42b0",
117119
"opentelemetry-instrumentation-aiohttp-server>=0.55b0",
118120
"opentelemetry-instrumentation-asgi>=0.42b0",

0 commit comments

Comments
 (0)