@@ -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
0 commit comments