Refactoring Opportunity
Summary
- File:
containers/api-proxy/otel.js
- Current size: 541 lines
- Responsibilities identified: 4 distinct concerns
Evidence
otel.js is the OpenTelemetry integration for the api-proxy. It was written as a single module but has grown to contain four independent layers:
1. OTLP serialization utilities (lines 77–186, ~110 lines)
Ten pure functions that convert Node.js OpenTelemetry SDK objects into OTLP protobuf-style wire format. They have no side effects and no dependency on global state:
function parseOtlpHeaders(raw) { ... }
function hrTimeToNanoString(hrTime) { ... }
function serializeAttrValue(val) { ... }
function serializeAttributes(attrs) { ... }
function serializeEvent(event) { ... }
function toOtlpKind(kind) { ... }
function serializeStatus(status) { ... }
function serializeSpan(span) { ... }
function buildResourceSpans(spans, resource) { ... }
2. Span exporter classes (lines 187–342, ~156 lines)
Two independent SpanExporter implementations:
ProxyAwareOtlpExporter (~99 lines, lines 187–285): HTTP/HTTPS exporter with proxy-agent support, OTLP batching, and retry logic. Its size alone triggers the functions->80-lines threshold.
FileSpanExporter (~57 lines, lines 286–342): writes spans to a JSON-lines log file. A completely independent strategy.
Both classes depend only on the serialization utilities above, not on the global tracer state initialized below.
3. Tracer initialization and context building (lines 343–410, ~68 lines)
_init() and _buildParentContext() set up the global NodeTracerProvider and wire the exporters to span processors. This is the only section that mutates module-level state (_provider, _tracer, _enabled).
4. Span lifecycle API (lines 411–542, ~132 lines)
Public functions consumed by proxy-request.js and server.js:
function startRequestSpan({ provider, method, path, requestId }) { ... } // ~34 lines
function setTokenAttributes(span, { ... }) { ... } // ~32 lines
function endSpan(span, statusCode) { ... } // ~20 lines
function endSpanError(span, err, statusCode) { ... } // ~29 lines
function shutdown() { ... }
function isEnabled() { ... }
Proposed Split
| New module |
Content |
Est. lines |
otel-serialization.js |
parseOtlpHeaders, hrTimeToNanoString, serializeAttrValue, serializeAttributes, serializeEvent, toOtlpKind, serializeStatus, serializeSpan, buildResourceSpans |
~115 |
otel-exporters.js |
ProxyAwareOtlpExporter, FileSpanExporter (requires otel-serialization.js) |
~160 |
otel.js (trimmed) |
_init, _buildParentContext, startRequestSpan, setTokenAttributes, endSpan, endSpanError, shutdown, isEnabled |
~270 |
otel.js would require('./otel-serialization') and require('./otel-exporters') in its _init, keeping all call sites unchanged.
Affected Callers
grep -rn "require.*otel\|from.*otel" containers/api-proxy/ 2>/dev/null | grep -v test
Current callers:
containers/api-proxy/proxy-request.js — otel.startRequestSpan, otel.endSpan, otel.endSpanError
containers/api-proxy/server.js — otelShutdown
Neither caller needs the exporter classes or serialization layer directly; the public API surface is unchanged.
Effort Estimate
Low — no logic changes required; only require() declarations and module.exports need updating.
Benefits
- Serialization utilities become independently testable without instantiating a provider or exporter
ProxyAwareOtlpExporter (99 lines) and FileSpanExporter (57 lines) get their own file with focused tests
- OTLP serialization is a maintenance hotspot whenever the Copilot LLM usage schema changes; isolating it reduces cognitive load during updates
otel.js drops from 541 to ~270 lines while retaining its role as the module's public facade
Detected by Refactoring Scanner workflow. Run date: 2026-06-05
Generated by Refactoring Opportunity Scanner · sonnet46 3.2M · ◷
Refactoring Opportunity
Summary
containers/api-proxy/otel.jsEvidence
otel.jsis the OpenTelemetry integration for the api-proxy. It was written as a single module but has grown to contain four independent layers:1. OTLP serialization utilities (lines 77–186, ~110 lines)
Ten pure functions that convert Node.js OpenTelemetry SDK objects into OTLP protobuf-style wire format. They have no side effects and no dependency on global state:
2. Span exporter classes (lines 187–342, ~156 lines)
Two independent
SpanExporterimplementations:ProxyAwareOtlpExporter(~99 lines, lines 187–285): HTTP/HTTPS exporter with proxy-agent support, OTLP batching, and retry logic. Its size alone triggers the functions->80-lines threshold.FileSpanExporter(~57 lines, lines 286–342): writes spans to a JSON-lines log file. A completely independent strategy.Both classes depend only on the serialization utilities above, not on the global tracer state initialized below.
3. Tracer initialization and context building (lines 343–410, ~68 lines)
_init()and_buildParentContext()set up the globalNodeTracerProviderand wire the exporters to span processors. This is the only section that mutates module-level state (_provider,_tracer,_enabled).4. Span lifecycle API (lines 411–542, ~132 lines)
Public functions consumed by
proxy-request.jsandserver.js:Proposed Split
otel-serialization.jsparseOtlpHeaders,hrTimeToNanoString,serializeAttrValue,serializeAttributes,serializeEvent,toOtlpKind,serializeStatus,serializeSpan,buildResourceSpansotel-exporters.jsProxyAwareOtlpExporter,FileSpanExporter(requiresotel-serialization.js)otel.js(trimmed)_init,_buildParentContext,startRequestSpan,setTokenAttributes,endSpan,endSpanError,shutdown,isEnabledotel.jswouldrequire('./otel-serialization')andrequire('./otel-exporters')in its_init, keeping all call sites unchanged.Affected Callers
Current callers:
containers/api-proxy/proxy-request.js—otel.startRequestSpan,otel.endSpan,otel.endSpanErrorcontainers/api-proxy/server.js—otelShutdownNeither caller needs the exporter classes or serialization layer directly; the public API surface is unchanged.
Effort Estimate
Low — no logic changes required; only
require()declarations andmodule.exportsneed updating.Benefits
ProxyAwareOtlpExporter(99 lines) andFileSpanExporter(57 lines) get their own file with focused testsotel.jsdrops from 541 to ~270 lines while retaining its role as the module's public facadeDetected by Refactoring Scanner workflow. Run date: 2026-06-05