11// @effect -diagnostics nodeBuiltinImport:off
22import * as NodeFS from "node:fs" ;
33import * as NodeOS from "node:os" ;
4+ import * as NodePath from "node:path" ;
45
56import { assert , expect , it } from "@effect/vitest" ;
67import * as ConfigProvider from "effect/ConfigProvider" ;
@@ -18,6 +19,7 @@ import {
1819import * as NetService from "@t3tools/shared/Net" ;
1920import * as NodeServices from "@effect/platform-node/NodeServices" ;
2021import { deriveServerPaths } from "../config.ts" ;
22+ import * as OtelEnvironment from "../observability/OtelEnvironment.ts" ;
2123import { resolveServerConfig } from "./config.ts" ;
2224
2325const deriveExplicitServerPaths = ( baseDir : string , devUrl : URL | undefined ) =>
@@ -49,7 +51,9 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
4951 otlpTracesUrl : undefined ,
5052 otlpMetricsUrl : undefined ,
5153 otlpExportIntervalMs : 10_000 ,
54+ otlpMetricsExportIntervalMs : 10_000 ,
5255 otlpServiceName : "t3-server" ,
56+ otelEnvironment : OtelEnvironment . none ,
5357 devAllowedOrigins : [ ] ,
5458 } as const ;
5559
@@ -488,6 +492,161 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
488492 } ) ,
489493 ) ;
490494
495+ // Resolving a config reads the settings file and creates the trace
496+ // directory, so a shared home would let one case see another's writes and
497+ // would race when these run in parallel.
498+ const resolveWithEnv = ( env : Record < string , string > ) => {
499+ const baseDir = NodeFS . mkdtempSync ( NodePath . join ( NodeOS . tmpdir ( ) , "t3code-otel-config-" ) ) ;
500+ return resolveServerConfig (
501+ {
502+ mode : Option . some ( "web" ) ,
503+ port : Option . some ( 4888 ) ,
504+ host : Option . none ( ) ,
505+ baseDir : Option . some ( baseDir ) ,
506+ cwd : Option . none ( ) ,
507+ devUrl : Option . none ( ) ,
508+ noBrowser : Option . none ( ) ,
509+ bootstrapFd : Option . none ( ) ,
510+ autoBootstrapProjectFromCwd : Option . none ( ) ,
511+ logWebSocketEvents : Option . none ( ) ,
512+ tailscaleServeEnabled : Option . none ( ) ,
513+ tailscaleServePort : Option . none ( ) ,
514+ } ,
515+ Option . none ( ) ,
516+ ) . pipe (
517+ Effect . provide (
518+ Layer . mergeAll ( ConfigProvider . layer ( ConfigProvider . fromEnv ( { env } ) ) , NetService . layer ) ,
519+ ) ,
520+ Effect . ensuring (
521+ Effect . sync ( ( ) => {
522+ NodeFS . rmSync ( baseDir , { recursive : true , force : true } ) ;
523+ } ) ,
524+ ) ,
525+ ) ;
526+ } ;
527+
528+ it . effect ( "exports to the endpoint the rest of the machine already uses" , ( ) =>
529+ Effect . gen ( function * ( ) {
530+ const resolved = yield * resolveWithEnv ( {
531+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
532+ OTEL_SERVICE_NAME : "t3" ,
533+ } ) ;
534+
535+ expect ( resolved . otlpTracesUrl ) . toBe ( "https://collector.example.com/v1/traces" ) ;
536+ expect ( resolved . otlpMetricsUrl ) . toBe ( "https://collector.example.com/v1/metrics" ) ;
537+ expect ( resolved . otlpServiceName ) . toBe ( "t3" ) ;
538+ } ) ,
539+ ) ;
540+
541+ it . effect ( "does not let an empty T3 Code name stand in for an answer" , ( ) =>
542+ Effect . gen ( function * ( ) {
543+ // An empty variable is set without saying anything. Reading it as an
544+ // answer would publish an endpoint nothing can reach and would suppress
545+ // the ambient one that could have been used instead.
546+ const resolved = yield * resolveWithEnv ( {
547+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
548+ OTEL_SERVICE_NAME : "t3" ,
549+ T3CODE_OTLP_TRACES_URL : "" ,
550+ T3CODE_OTLP_METRICS_URL : " " ,
551+ T3CODE_OTLP_SERVICE_NAME : "" ,
552+ } ) ;
553+
554+ expect ( resolved . otlpTracesUrl ) . toBe ( "https://collector.example.com/v1/traces" ) ;
555+ expect ( resolved . otlpMetricsUrl ) . toBe ( "https://collector.example.com/v1/metrics" ) ;
556+ expect ( resolved . otlpServiceName ) . toBe ( "t3" ) ;
557+ } ) ,
558+ ) ;
559+
560+ it . effect ( "keeps T3 Code's own names as the explicit answer" , ( ) =>
561+ Effect . gen ( function * ( ) {
562+ const resolved = yield * resolveWithEnv ( {
563+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
564+ OTEL_SERVICE_NAME : "t3" ,
565+ T3CODE_OTLP_TRACES_URL : "http://localhost:4318/v1/traces" ,
566+ T3CODE_OTLP_SERVICE_NAME : "t3-local" ,
567+ } ) ;
568+
569+ expect ( resolved . otlpTracesUrl ) . toBe ( "http://localhost:4318/v1/traces" ) ;
570+ expect ( resolved . otlpMetricsUrl ) . toBe ( "https://collector.example.com/v1/metrics" ) ;
571+ expect ( resolved . otlpServiceName ) . toBe ( "t3-local" ) ;
572+ } ) ,
573+ ) ;
574+
575+ it . effect ( "leaves a T3 Code endpoint alone when the environment names another" , ( ) =>
576+ Effect . gen ( function * ( ) {
577+ // An ambient endpoint that lost the URL must not keep configuring the
578+ // export around it: its wire format, headers, and batching belong to the
579+ // endpoint it named, not to this one.
580+ const resolved = yield * resolveWithEnv ( {
581+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
582+ T3CODE_OTLP_TRACES_URL : "http://localhost:4318/v1/traces" ,
583+ } ) ;
584+
585+ expect ( resolved . otelEnvironment . traces . settings ) . toBeUndefined ( ) ;
586+ expect ( resolved . otelEnvironment . metrics . settings ?. url ) . toBe (
587+ "https://collector.example.com/v1/metrics" ,
588+ ) ;
589+ expect ( resolved . otlpExportIntervalMs ) . toBe ( 10_000 ) ;
590+ } ) ,
591+ ) ;
592+
593+ it . effect ( "keeps an ambient aggregation off a T3 Code metric endpoint" , ( ) =>
594+ Effect . gen ( function * ( ) {
595+ const resolved = yield * resolveWithEnv ( {
596+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
597+ OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE : "delta" ,
598+ T3CODE_OTLP_METRICS_URL : "http://localhost:4318/v1/metrics" ,
599+ } ) ;
600+
601+ expect ( resolved . otelEnvironment . metrics . settings ) . toBeUndefined ( ) ;
602+ expect ( resolved . otelEnvironment . traces . settings ?. temporality ) . toBeUndefined ( ) ;
603+ } ) ,
604+ ) ;
605+
606+ it . effect ( "keeps one signal's schedule off the other one" , ( ) =>
607+ Effect . gen ( function * ( ) {
608+ // Traces take the specification's five second batch delay from the
609+ // ambient endpoint. Metrics went somewhere else and keep T3 Code's own
610+ // interval rather than inheriting a number meant for spans.
611+ const resolved = yield * resolveWithEnv ( {
612+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
613+ T3CODE_OTLP_METRICS_URL : "http://localhost:4318/v1/metrics" ,
614+ } ) ;
615+
616+ expect ( resolved . otlpExportIntervalMs ) . toBe ( 5_000 ) ;
617+ expect ( resolved . otlpMetricsExportIntervalMs ) . toBe ( 10_000 ) ;
618+ } ) ,
619+ ) ;
620+
621+ it . effect ( "does not report a signal as declined while it is exporting" , ( ) =>
622+ Effect . gen ( function * ( ) {
623+ // grpc turns off the export these variables asked for, and says nothing
624+ // about a signal whose endpoint came from a T3 Code name.
625+ const resolved = yield * resolveWithEnv ( {
626+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
627+ OTEL_EXPORTER_OTLP_PROTOCOL : "grpc" ,
628+ T3CODE_OTLP_TRACES_URL : "http://localhost:4318/v1/traces" ,
629+ } ) ;
630+
631+ expect ( resolved . otlpTracesUrl ) . toBe ( "http://localhost:4318/v1/traces" ) ;
632+ expect ( resolved . otelEnvironment . traces . declined ) . toBeUndefined ( ) ;
633+ expect ( resolved . otelEnvironment . metrics . declined ) . toContain ( "grpc" ) ;
634+ } ) ,
635+ ) ;
636+
637+ it . effect ( "exports nothing at all once the SDK is switched off" , ( ) =>
638+ Effect . gen ( function * ( ) {
639+ const resolved = yield * resolveWithEnv ( {
640+ OTEL_SDK_DISABLED : "true" ,
641+ OTEL_EXPORTER_OTLP_ENDPOINT : "https://collector.example.com" ,
642+ T3CODE_OTLP_TRACES_URL : "http://localhost:4318/v1/traces" ,
643+ } ) ;
644+
645+ expect ( resolved . otlpTracesUrl ) . toBeUndefined ( ) ;
646+ expect ( resolved . otlpMetricsUrl ) . toBeUndefined ( ) ;
647+ } ) ,
648+ ) ;
649+
491650 it . effect ( "falls back to persisted observability settings when env vars are absent" , ( ) =>
492651 Effect . gen ( function * ( ) {
493652 const fs = yield * FileSystem . FileSystem ;
0 commit comments