Skip to content

Commit fa2ed4a

Browse files
committed
Add OTLP proxy endpoint
1 parent e5d1be7 commit fa2ed4a

20 files changed

+2252
-48
lines changed

Directory.Packages.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<PackageVersion Include="AWSSDK.S3" Version="4.0.7.14" />
2828
<PackageVersion Include="Elastic.OpenTelemetry" Version="1.1.0" />
2929
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0" />
30+
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
3031
<PackageVersion Include="Microsoft.Extensions.Telemetry.Abstractions" Version="10.0.0" />
3132
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.13.0" />
3233
<PackageVersion Include="Generator.Equals" Version="3.2.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
@@ -41,6 +42,7 @@
4142
<PackageVersion Include="Microsoft.OpenApi" Version="3.0.1" />
4243
<PackageVersion Include="TUnit" Version="0.25.21" />
4344
<PackageVersion Include="xunit.v3.extensibility.core" Version="2.0.2" />
45+
<PackageVersion Include="WireMock.Net" Version="1.6.11" />
4446
</ItemGroup>
4547
<!-- Build -->
4648
<ItemGroup>
@@ -106,4 +108,4 @@
106108
</PackageVersion>
107109
<PackageVersion Include="xunit.v3" Version="2.0.2" />
108110
</ItemGroup>
109-
</Project>
111+
</Project>

src/api/Elastic.Documentation.Api.Core/Elastic.Documentation.Api.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
1213
<PackageReference Include="Microsoft.Extensions.Logging" />
1314
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions" />
1415
</ItemGroup>
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
# OTLP Proxy for Frontend Telemetry with ADOT Lambda Layer
2+
3+
This OTLP (OpenTelemetry Protocol) proxy allows frontend JavaScript code to send telemetry (logs, traces, metrics) through the ADOT (AWS Distro for OpenTelemetry) Lambda Layer **without exposing authentication credentials to the browser**.
4+
5+
## Architecture with ADOT Lambda Layer
6+
7+
```
8+
Frontend (Browser) → API Proxy → ADOT Lambda Layer (localhost:4318) → Elastic APM
9+
10+
Handles authentication
11+
(credentials in env vars)
12+
```
13+
14+
### Benefits
15+
16+
1. **Secure**: Frontend never sees authentication credentials
17+
2. **Simple**: ADOT layer handles all backend authentication
18+
3. **Fast**: Local proxy (no external network hop in Lambda)
19+
4. **Standard**: Uses OpenTelemetry Protocol (OTLP) - works with any OTEL SDK
20+
21+
## Lambda Configuration
22+
23+
### Required Environment Variables
24+
25+
```bash
26+
# Enable ADOT Lambda Layer
27+
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
28+
29+
# Configure where ADOT forwards telemetry (your Elastic APM endpoint)
30+
OTEL_EXPORTER_OTLP_ENDPOINT=https://your-apm-server.elastic.co:443
31+
32+
# Authentication for the backend (ADOT uses this, not exposed to frontend)
33+
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-secret-token"
34+
# Or for Elastic Cloud:
35+
# OTEL_EXPORTER_OTLP_HEADERS="Authorization=ApiKey base64-api-key"
36+
37+
# Service name for backend telemetry
38+
OTEL_SERVICE_NAME=docs-api
39+
40+
# Resource attributes
41+
OTEL_RESOURCE_ATTRIBUTES="deployment.environment=prod,service.version=1.0.0"
42+
```
43+
44+
### Lambda Layer ARN
45+
46+
Add the ADOT Lambda Layer to your Lambda function. Find the latest ARN at:
47+
https://aws-otel.github.io/docs/getting-started/lambda/lambda-dotnet
48+
49+
Example for us-east-1:
50+
```
51+
arn:aws:lambda:us-east-1:901920570463:layer:aws-otel-collector-amd64-ver-0-90-1:1
52+
```
53+
54+
## API Endpoints
55+
56+
The proxy provides three endpoints:
57+
58+
```
59+
POST /_api/v1/otlp/v1/traces - Forward trace spans
60+
POST /_api/v1/otlp/v1/logs - Forward log records
61+
POST /_api/v1/otlp/v1/metrics - Forward metrics
62+
```
63+
64+
##Frontend Usage
65+
66+
### Quick Start with OpenTelemetry JS
67+
68+
```typescript
69+
import { WebTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
70+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
71+
import { Resource } from '@opentelemetry/resources';
72+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
73+
74+
// Configure tracer to use the proxy endpoint
75+
const provider = new WebTracerProvider({
76+
resource: new Resource({
77+
[SemanticResourceAttributes.SERVICE_NAME]: 'docs-frontend',
78+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]:
79+
window.location.hostname.includes('localhost') ? 'dev' : 'prod',
80+
}),
81+
});
82+
83+
// Point to the proxy - no credentials needed!
84+
const exporter = new OTLPTraceExporter({
85+
url: 'https://docs.elastic.co/_api/v1/otlp/v1/traces',
86+
// No Authorization header needed - proxy handles it!
87+
});
88+
89+
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
90+
provider.register();
91+
92+
// Now create spans
93+
const tracer = provider.getTracer('docs-frontend');
94+
const span = tracer.startSpan('page-load');
95+
span.setAttribute('page.url', window.location.href);
96+
span.end();
97+
```
98+
99+
### Logs from Frontend
100+
101+
```typescript
102+
import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
103+
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
104+
105+
const loggerProvider = new LoggerProvider({
106+
resource: new Resource({
107+
[SemanticResourceAttributes.SERVICE_NAME]: 'docs-frontend',
108+
}),
109+
});
110+
111+
const exporter = new OTLPLogExporter({
112+
url: 'https://docs.elastic.co/_api/v1/otlp/v1/logs',
113+
});
114+
115+
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter));
116+
117+
const logger = loggerProvider.getLogger('docs-frontend');
118+
119+
// Log user actions
120+
logger.emit({
121+
severityNumber: 9, // INFO
122+
severityText: 'INFO',
123+
body: 'User clicked search button',
124+
attributes: {
125+
'user.action': 'search',
126+
'search.query': 'elasticsearch',
127+
},
128+
});
129+
```
130+
131+
### Complete Frontend Integration
132+
133+
```typescript
134+
// telemetry.ts - Initialize once at app startup
135+
import { trace } from '@opentelemetry/api';
136+
import { WebTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
137+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
138+
import { ZoneContextManager } from '@opentelemetry/context-zone';
139+
import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
140+
import { UserInteractionInstrumentation } from '@opentelemetry/instrumentation-user-interaction';
141+
import { registerInstrumentations } from '@opentelemetry/instrumentation';
142+
import { Resource } from '@opentelemetry/resources';
143+
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
144+
145+
export function initTelemetry() {
146+
const provider = new WebTracerProvider({
147+
resource: new Resource({
148+
[SemanticResourceAttributes.SERVICE_NAME]: 'docs-frontend',
149+
[SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
150+
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]:
151+
window.location.hostname === 'docs.elastic.co' ? 'prod' : 'dev',
152+
}),
153+
});
154+
155+
// Use the proxy endpoint (same origin, no CORS issues!)
156+
const exporter = new OTLPTraceExporter({
157+
url: `${window.location.origin}/_api/v1/otlp/v1/traces`,
158+
});
159+
160+
provider.addSpanProcessor(new BatchSpanProcessor(exporter, {
161+
maxQueueSize: 100,
162+
maxExportBatchSize: 10,
163+
scheduledDelayMillis: 5000, // Send every 5 seconds
164+
}));
165+
166+
provider.register({
167+
contextManager: new ZoneContextManager(),
168+
});
169+
170+
// Auto-instrument page loads and clicks
171+
registerInstrumentations({
172+
instrumentations: [
173+
new DocumentLoadInstrumentation(),
174+
new UserInteractionInstrumentation({
175+
eventNames: ['click', 'submit'],
176+
}),
177+
],
178+
});
179+
180+
console.log('OpenTelemetry initialized via ADOT proxy');
181+
}
182+
183+
// Call this in your app entry point
184+
initTelemetry();
185+
186+
// Now you can manually create spans anywhere
187+
const tracer = trace.getTracer('docs-frontend');
188+
const span = tracer.startSpan('search-request');
189+
span.setAttribute('search.query', 'elasticsearch');
190+
span.end();
191+
```
192+
193+
## How It Works
194+
195+
### Request Flow
196+
197+
1. **Frontend** sends OTLP JSON payload to `/_api/v1/otlp/v1/traces`
198+
2. **API Proxy** forwards to `http://localhost:4318/v1/traces` (ADOT layer)
199+
3. **ADOT Layer** adds authentication headers and forwards to Elastic APM
200+
4. **Elastic APM** receives and indexes the telemetry
201+
202+
### Security
203+
204+
- Frontend code **never** contains authentication credentials
205+
- Credentials live in Lambda environment variables (encrypted at rest)
206+
- ADOT layer handles all authentication to Elastic APM
207+
- Proxy acts as a secure gateway
208+
209+
## Monitoring
210+
211+
### Proxy Telemetry
212+
213+
The proxy itself creates spans you can monitor:
214+
215+
- **Activity Source**: `Elastic.Documentation.Api.OtlpProxy`
216+
- **Tags**:
217+
- `otel.signal_type` - traces/logs/metrics
218+
- `otel.content_type` - application/json or application/x-protobuf
219+
- `otel.adot_endpoint` - http://localhost:4318
220+
- `otel.target_url` - Full URL forwarded to
221+
- `http.response.status_code` - Response from ADOT
222+
223+
### CloudWatch Logs
224+
225+
```bash
226+
# Search for proxy logs
227+
aws logs tail /aws/lambda/docs-api --filter "ProxyOtlp" --follow
228+
```
229+
230+
## Troubleshooting
231+
232+
### Connection refused to localhost:4318
233+
234+
**Cause**: ADOT Lambda Layer is not enabled
235+
**Solution**: Ensure `AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument` is set
236+
237+
### Telemetry not appearing in Elastic APM
238+
239+
1. Check ADOT layer logs in CloudWatch
240+
2. Verify `OTEL_EXPORTER_OTLP_ENDPOINT` points to your APM server
241+
3. Verify `OTEL_EXPORTER_OTLP_HEADERS` contains valid credentials
242+
4. Check APM server is reachable from Lambda (VPC/security groups)
243+
244+
### CORS errors from frontend
245+
246+
If your docs are served from a different domain than the API:
247+
248+
```csharp
249+
// In Program.cs
250+
app.UseCors(policy => policy
251+
.WithOrigins("https://docs.elastic.co")
252+
.AllowAnyMethod()
253+
.AllowAnyHeader());
254+
```
255+
256+
### High Lambda costs
257+
258+
- Use **BatchSpanProcessor** in frontend (batches multiple spans)
259+
- Set reasonable `scheduledDelayMillis` (5-10 seconds)
260+
- Consider sampling in production:
261+
262+
```typescript
263+
import { TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';
264+
265+
const provider = new WebTracerProvider({
266+
sampler: new TraceIdRatioBasedSampler(0.1), // Sample 10% of traces
267+
// ...
268+
});
269+
```
270+
271+
## Performance Considerations
272+
273+
- **Batching**: Frontend batches telemetry before sending
274+
- **No buffering**: Proxy streams data (no memory overhead)
275+
- **Local forwarding**: ADOT on localhost:4318 (no network latency)
276+
- **Async**: ADOT forwards asynchronously to backend
277+
278+
## Example: Track Search Queries
279+
280+
```typescript
281+
import { trace } from '@opentelemetry/api';
282+
283+
function handleSearch(query: string) {
284+
const tracer = trace.getTracer('docs-frontend');
285+
const span = tracer.startSpan('search', {
286+
attributes: {
287+
'search.query': query,
288+
'search.source': 'search-box',
289+
},
290+
});
291+
292+
try {
293+
// Perform search
294+
const results = await fetch(`/api/search?q=${query}`);
295+
span.setAttribute('search.results_count', results.length);
296+
span.setStatus({ code: SpanStatusCode.OK });
297+
} catch (error) {
298+
span.setStatus({
299+
code: SpanStatusCode.ERROR,
300+
message: error.message,
301+
});
302+
} finally {
303+
span.end();
304+
}
305+
}
306+
```
307+
308+
## Next Steps
309+
310+
1. **Deploy**: Ensure ADOT layer is attached to your Lambda
311+
2. **Configure**: Set `AWS_LAMBDA_EXEC_WRAPPER` and OTEL environment variables
312+
3. **Instrument**: Add OpenTelemetry SDK to your frontend
313+
4. **Test**: Send test telemetry from browser DevTools
314+
5. **Monitor**: Check Elastic APM for incoming frontend telemetry

0 commit comments

Comments
 (0)