@opensearch-project/agent-health 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,571 @@
1
+ # Copyright OpenSearch Contributors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Agent Health — CloudFormation template for managed observability infrastructure.
5
+ # Deploys an Amazon OpenSearch Service domain and OpenSearch Ingestion (OSIS) pipeline
6
+ # for collecting OpenTelemetry traces from AI agents.
7
+ #
8
+ # Launch Stack URL pattern (replace REGION):
9
+ # https://console.aws.amazon.com/cloudformation/home?region=REGION#/stacks/create/template?stackName=AgentHealthObservability&templateURL=https://agent-health-cfn-REGION.s3.REGION.amazonaws.com/agent-health-observability.yaml
10
+
11
+ AWSTemplateFormatVersion: '2010-09-09'
12
+ Description: >-
13
+ Agent Health Observability Stack — Amazon OpenSearch Service domain with
14
+ OpenSearch Ingestion (OSIS) pipeline for OTLP trace collection from AI agents.
15
+
16
+ # =============================================================================
17
+ # Parameters
18
+ # =============================================================================
19
+ Parameters:
20
+ DomainName:
21
+ Type: String
22
+ Default: agent-health-traces
23
+ Description: Name for the OpenSearch Service domain
24
+ AllowedPattern: '[a-z][a-z0-9\-]+'
25
+ MinLength: 3
26
+ MaxLength: 28
27
+
28
+ InstanceType:
29
+ Type: String
30
+ Default: r8g.large.search
31
+ Description: OpenSearch instance type
32
+ AllowedValues:
33
+ - t3.small.search
34
+ - t3.medium.search
35
+ - r8g.large.search
36
+ - r8g.xlarge.search
37
+ - r8g.2xlarge.search
38
+
39
+ VolumeSize:
40
+ Type: Number
41
+ Default: 100
42
+ Description: EBS volume size in GB per data node
43
+ MinValue: 10
44
+ MaxValue: 1000
45
+
46
+ PipelineMinUnits:
47
+ Type: Number
48
+ Default: 1
49
+ Description: Minimum OSIS pipeline capacity units
50
+ MinValue: 1
51
+ MaxValue: 96
52
+
53
+ PipelineMaxUnits:
54
+ Type: Number
55
+ Default: 4
56
+ Description: Maximum OSIS pipeline capacity units
57
+ MinValue: 1
58
+ MaxValue: 96
59
+
60
+ MasterUserARN:
61
+ Type: String
62
+ Description: >-
63
+ IAM ARN for the OpenSearch FGAC master user (used for security plugin admin).
64
+ Defaults to the account root; override with your IAM role ARN for direct access.
65
+ Default: ''
66
+
67
+ # =============================================================================
68
+ # Conditions
69
+ # =============================================================================
70
+ Conditions:
71
+ HasMasterUserARN: !Not [!Equals [!Ref MasterUserARN, '']]
72
+
73
+ # =============================================================================
74
+ # Resources
75
+ # =============================================================================
76
+ Resources:
77
+
78
+ # ---------------------------------------------------------------------------
79
+ # OpenSearch Service Domain
80
+ # ---------------------------------------------------------------------------
81
+ OpenSearchDomain:
82
+ Type: AWS::OpenSearchService::Domain
83
+ Properties:
84
+ DomainName: !Ref DomainName
85
+ EngineVersion: OpenSearch_3.5
86
+ ClusterConfig:
87
+ InstanceType: !Ref InstanceType
88
+ InstanceCount: 3
89
+ DedicatedMasterEnabled: true
90
+ DedicatedMasterType: !Ref InstanceType
91
+ DedicatedMasterCount: 3
92
+ ZoneAwarenessEnabled: true
93
+ ZoneAwarenessConfig:
94
+ AvailabilityZoneCount: 3
95
+ EBSOptions:
96
+ EBSEnabled: true
97
+ VolumeType: gp3
98
+ VolumeSize: !Ref VolumeSize
99
+ EncryptionAtRestOptions:
100
+ Enabled: true
101
+ NodeToNodeEncryptionOptions:
102
+ Enabled: true
103
+ DomainEndpointOptions:
104
+ EnforceHTTPS: true
105
+ TLSSecurityPolicy: Policy-Min-TLS-1-2-PFS-2023-10
106
+ AdvancedSecurityOptions:
107
+ Enabled: true
108
+ InternalUserDatabaseEnabled: false
109
+ MasterUserOptions:
110
+ MasterUserARN: !If
111
+ - HasMasterUserARN
112
+ - !Ref MasterUserARN
113
+ - !Sub 'arn:aws:iam::${AWS::AccountId}:root'
114
+ # "Only use fine-grained access control" — open domain policy,
115
+ # all authorization is handled by the OpenSearch Security plugin (FGAC).
116
+ AccessPolicies:
117
+ Version: '2012-10-17'
118
+ Statement:
119
+ - Effect: Allow
120
+ Principal:
121
+ AWS: '*'
122
+ Action: 'es:*'
123
+ Resource: !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${DomainName}/*'
124
+ Tags:
125
+ - Key: agent-health
126
+ Value: observability
127
+ - Key: ManagedBy
128
+ Value: AgentHealthCFN
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # IAM Role — OSIS Pipeline Execution
132
+ # ---------------------------------------------------------------------------
133
+ OSISPipelineRole:
134
+ Type: AWS::IAM::Role
135
+ Properties:
136
+ RoleName: !Sub '${DomainName}-osis-pipeline-role'
137
+ AssumeRolePolicyDocument:
138
+ Version: '2012-10-17'
139
+ Statement:
140
+ - Effect: Allow
141
+ Principal:
142
+ Service: osis-pipelines.amazonaws.com
143
+ Action: 'sts:AssumeRole'
144
+ Policies:
145
+ - PolicyName: OSISToOpenSearch
146
+ PolicyDocument:
147
+ Version: '2012-10-17'
148
+ Statement:
149
+ - Effect: Allow
150
+ Action:
151
+ - 'es:DescribeDomain'
152
+ - 'es:ESHttp*'
153
+ Resource:
154
+ - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${DomainName}'
155
+ - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${DomainName}/*'
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # IAM Role — Ingestion (for agents pushing telemetry via SigV4)
159
+ # ---------------------------------------------------------------------------
160
+ IngestionRole:
161
+ Type: AWS::IAM::Role
162
+ Properties:
163
+ RoleName: !Sub '${DomainName}-ingestion-role'
164
+ AssumeRolePolicyDocument:
165
+ Version: '2012-10-17'
166
+ Statement:
167
+ - Effect: Allow
168
+ Principal:
169
+ AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
170
+ Action: 'sts:AssumeRole'
171
+ Policies:
172
+ - PolicyName: OSISIngest
173
+ PolicyDocument:
174
+ Version: '2012-10-17'
175
+ Statement:
176
+ - Effect: Allow
177
+ Action:
178
+ - 'osis:Ingest'
179
+ Resource:
180
+ - !Sub 'arn:aws:osis:${AWS::Region}:${AWS::AccountId}:pipeline/${DomainName}-traces'
181
+ - !Sub 'arn:aws:osis:${AWS::Region}:${AWS::AccountId}:pipeline/${DomainName}-logs'
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # OpenSearch Ingestion (OSIS) Pipeline — OTLP Traces → OpenSearch
185
+ # ---------------------------------------------------------------------------
186
+ OSISTracePipeline:
187
+ Type: AWS::OSIS::Pipeline
188
+ DependsOn: OpenSearchDomain
189
+ Properties:
190
+ PipelineName: !Sub '${DomainName}-traces'
191
+ MinUnits: !Ref PipelineMinUnits
192
+ MaxUnits: !Ref PipelineMaxUnits
193
+ PipelineConfigurationBody: !Sub |
194
+ version: "2"
195
+ otel-traces-entry:
196
+ source:
197
+ otel_trace_source:
198
+ path: "/${DomainName}-traces/v1/traces"
199
+ sink:
200
+ - pipeline:
201
+ name: "otel-traces-raw-pipeline"
202
+ - pipeline:
203
+ name: "otel-service-map-pipeline"
204
+ otel-traces-raw-pipeline:
205
+ source:
206
+ pipeline:
207
+ name: "otel-traces-entry"
208
+ processor:
209
+ - otel_traces:
210
+ sink:
211
+ - opensearch:
212
+ hosts:
213
+ - "https://${OpenSearchDomain.DomainEndpoint}"
214
+ index_type: trace-analytics-raw
215
+ aws:
216
+ sts_role_arn: "${OSISPipelineRole.Arn}"
217
+ region: "${AWS::Region}"
218
+ otel-service-map-pipeline:
219
+ source:
220
+ pipeline:
221
+ name: "otel-traces-entry"
222
+ processor:
223
+ - service_map:
224
+ window_duration: 180
225
+ sink:
226
+ - opensearch:
227
+ hosts:
228
+ - "https://${OpenSearchDomain.DomainEndpoint}"
229
+ index_type: trace-analytics-service-map
230
+ aws:
231
+ sts_role_arn: "${OSISPipelineRole.Arn}"
232
+ region: "${AWS::Region}"
233
+ Tags:
234
+ - Key: agent-health
235
+ Value: observability
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # OpenSearch Ingestion (OSIS) Pipeline — OTLP Logs → OpenSearch
239
+ # ---------------------------------------------------------------------------
240
+ OSISLogsPipeline:
241
+ Type: AWS::OSIS::Pipeline
242
+ DependsOn: OpenSearchDomain
243
+ Properties:
244
+ PipelineName: !Sub '${DomainName}-logs'
245
+ MinUnits: !Ref PipelineMinUnits
246
+ MaxUnits: !Ref PipelineMaxUnits
247
+ PipelineConfigurationBody: !Sub |
248
+ version: "2"
249
+ otel-logs-pipeline:
250
+ source:
251
+ otel_logs_source:
252
+ path: "/${DomainName}-logs/v1/logs"
253
+ sink:
254
+ - opensearch:
255
+ hosts:
256
+ - "https://${OpenSearchDomain.DomainEndpoint}"
257
+ index: "otel-logs-%%{yyyy.MM.dd}"
258
+ aws:
259
+ sts_role_arn: "${OSISPipelineRole.Arn}"
260
+ region: "${AWS::Region}"
261
+ Tags:
262
+ - Key: agent-health
263
+ Value: observability
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # OTLP Ingestion — API Gateway + Lambda → OpenSearch (direct)
267
+ # Provides a public HTTPS endpoint so apps can send OTLP without SigV4 auth.
268
+ # Converts OTLP JSON to trace-analytics format and bulk-indexes into OpenSearch.
269
+ # Also forwards to OSIS pipelines when available for service-map generation.
270
+ # ---------------------------------------------------------------------------
271
+ OTLPIngestLambdaRole:
272
+ Type: AWS::IAM::Role
273
+ Properties:
274
+ RoleName: !Sub '${DomainName}-otlp-ingest-lambda-role'
275
+ AssumeRolePolicyDocument:
276
+ Version: '2012-10-17'
277
+ Statement:
278
+ - Effect: Allow
279
+ Principal:
280
+ Service: lambda.amazonaws.com
281
+ Action: 'sts:AssumeRole'
282
+ Policies:
283
+ - PolicyName: OpenSearchAccess
284
+ PolicyDocument:
285
+ Version: '2012-10-17'
286
+ Statement:
287
+ - Effect: Allow
288
+ Action:
289
+ - 'es:ESHttp*'
290
+ Resource:
291
+ - !Sub 'arn:aws:es:${AWS::Region}:${AWS::AccountId}:domain/${DomainName}/*'
292
+ - PolicyName: OSISIngest
293
+ PolicyDocument:
294
+ Version: '2012-10-17'
295
+ Statement:
296
+ - Effect: Allow
297
+ Action:
298
+ - 'osis:Ingest'
299
+ Resource:
300
+ - !Sub 'arn:aws:osis:${AWS::Region}:${AWS::AccountId}:pipeline/${DomainName}-traces'
301
+ - !Sub 'arn:aws:osis:${AWS::Region}:${AWS::AccountId}:pipeline/${DomainName}-logs'
302
+ - PolicyName: CloudWatchLogs
303
+ PolicyDocument:
304
+ Version: '2012-10-17'
305
+ Statement:
306
+ - Effect: Allow
307
+ Action:
308
+ - 'logs:CreateLogGroup'
309
+ - 'logs:CreateLogStream'
310
+ - 'logs:PutLogEvents'
311
+ Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
312
+
313
+ OTLPIngestFunction:
314
+ Type: AWS::Lambda::Function
315
+ DependsOn: OpenSearchDomain
316
+ Properties:
317
+ FunctionName: !Sub '${DomainName}-otlp-ingest'
318
+ Runtime: python3.12
319
+ Handler: index.handler
320
+ Timeout: 30
321
+ MemorySize: 256
322
+ Role: !GetAtt OTLPIngestLambdaRole.Arn
323
+ Environment:
324
+ Variables:
325
+ OPENSEARCH_ENDPOINT: !Sub 'https://${OpenSearchDomain.DomainEndpoint}'
326
+ Code:
327
+ ZipFile: |
328
+ """OTLP Ingestion — converts OTLP JSON to OpenSearch trace-analytics format."""
329
+ import json
330
+ import os
331
+ import urllib.request
332
+ import hashlib
333
+ from datetime import datetime, timezone
334
+ from botocore.auth import SigV4Auth
335
+ from botocore.awsrequest import AWSRequest
336
+ from botocore.session import Session
337
+
338
+ OS_ENDPOINT = os.environ['OPENSEARCH_ENDPOINT']
339
+ REGION = os.environ.get('AWS_REGION', 'us-east-1')
340
+ session = Session()
341
+
342
+ STATUS_MAP = {0: 0, 1: 0, 2: 2} # OTLP Unset/Ok->0, Error->2
343
+ KIND_MAP = {0: 'SPAN_KIND_UNSPECIFIED', 1: 'SPAN_KIND_INTERNAL', 2: 'SPAN_KIND_SERVER',
344
+ 3: 'SPAN_KIND_CLIENT', 4: 'SPAN_KIND_PRODUCER', 5: 'SPAN_KIND_CONSUMER'}
345
+
346
+ def _creds():
347
+ c = session.get_credentials()
348
+ return c.resolve_credentials() if hasattr(c, 'resolve_credentials') else c
349
+
350
+ def _attr_value(v):
351
+ for k in ('stringValue', 'intValue', 'boolValue', 'doubleValue'):
352
+ if k in v:
353
+ return v[k]
354
+ if 'arrayValue' in v:
355
+ return [_attr_value(x) for x in v['arrayValue'].get('values', [])]
356
+ return str(v)
357
+
358
+ def _nano_to_iso(ns):
359
+ """Convert nanosecond timestamp to ISO 8601 with nanosecond precision."""
360
+ try:
361
+ ns = int(ns)
362
+ except (ValueError, TypeError):
363
+ return '1970-01-01T00:00:00.000000000Z'
364
+ secs = ns // 1_000_000_000
365
+ nanos = ns % 1_000_000_000
366
+ dt = datetime.fromtimestamp(secs, tz=timezone.utc)
367
+ return dt.strftime('%Y-%m-%dT%H:%M:%S') + f'.{nanos:09d}Z'
368
+
369
+ def _sanitize_key(key):
370
+ """Replace dots with @ in attribute keys (data prepper convention)."""
371
+ return key.replace('.', '@')
372
+
373
+ def _convert_span(span, resource_attrs, scope_name):
374
+ trace_id = span.get('traceId', '')
375
+ span_id = span.get('spanId', '')
376
+ parent = span.get('parentSpanId', '')
377
+ start_ns = span.get('startTimeUnixNano', '0')
378
+ end_ns = span.get('endTimeUnixNano', '0')
379
+ try:
380
+ duration_nanos = int(end_ns) - int(start_ns)
381
+ if duration_nanos < 0:
382
+ duration_nanos = 0
383
+ except (ValueError, TypeError):
384
+ duration_nanos = 0
385
+ status = span.get('status', {})
386
+ status_code = STATUS_MAP.get(status.get('code', 0), 0)
387
+ kind = KIND_MAP.get(span.get('kind', 0), 'SPAN_KIND_UNSPECIFIED')
388
+ service_name = 'unknown'
389
+ events = []
390
+ for evt in span.get('events', []):
391
+ evt_attrs = {}
392
+ for a in evt.get('attributes', []):
393
+ evt_attrs[_sanitize_key(a['key'])] = _attr_value(a['value'])
394
+ events.append({
395
+ 'name': evt.get('name', ''),
396
+ 'time': _nano_to_iso(evt.get('timeUnixNano', '0')),
397
+ 'attributes': evt_attrs
398
+ })
399
+ doc = {
400
+ 'traceId': trace_id,
401
+ 'spanId': span_id,
402
+ 'parentSpanId': parent,
403
+ 'traceState': span.get('traceState', ''),
404
+ 'name': span.get('name', ''),
405
+ 'kind': kind,
406
+ 'startTime': _nano_to_iso(start_ns),
407
+ 'endTime': _nano_to_iso(end_ns),
408
+ 'durationInNanos': duration_nanos,
409
+ 'serviceName': service_name,
410
+ 'status': {'code': status_code, 'message': status.get('message', '')},
411
+ 'events': events,
412
+ 'links': [],
413
+ 'instrumentationScope.name': scope_name,
414
+ }
415
+ if not parent:
416
+ doc['traceGroup'] = span.get('name', '')
417
+ doc['traceGroupFields.endTime'] = _nano_to_iso(end_ns)
418
+ doc['traceGroupFields.durationInNanos'] = duration_nanos
419
+ doc['traceGroupFields.statusCode'] = status_code
420
+ # Flatten resource attributes with @ separator for dotted keys
421
+ for a in resource_attrs:
422
+ key = _sanitize_key(a['key'])
423
+ val = _attr_value(a['value'])
424
+ doc[f'resource.attributes.{key}'] = val
425
+ if a['key'] == 'service.name':
426
+ doc['serviceName'] = val
427
+ # Flatten span attributes with @ separator for dotted keys
428
+ for a in span.get('attributes', []):
429
+ key = _sanitize_key(a['key'])
430
+ val = _attr_value(a['value'])
431
+ doc[f'span.attributes.{key}'] = val
432
+ doc_id = hashlib.md5(f'{trace_id}{span_id}'.encode()).hexdigest()
433
+ return doc_id, doc
434
+
435
+ def _signed_request(method, path, body):
436
+ url = OS_ENDPOINT + path
437
+ request = AWSRequest(method=method, url=url, data=body,
438
+ headers={'Content-Type': 'application/json'})
439
+ SigV4Auth(_creds(), 'es', REGION).add_auth(request)
440
+ req = urllib.request.Request(url, data=body.encode('utf-8'), method=method,
441
+ headers={k: v for k, v in dict(request.headers).items()})
442
+ try:
443
+ with urllib.request.urlopen(req) as resp:
444
+ return resp.status, resp.read().decode('utf-8', errors='replace')
445
+ except urllib.error.HTTPError as e:
446
+ return e.code, e.read().decode('utf-8', errors='replace')
447
+
448
+ def handler(event, context):
449
+ path = event.get('rawPath', '')
450
+ if '/v1/traces' not in path and '/v1/logs' not in path:
451
+ return {'statusCode': 404, 'body': json.dumps({'error': f'Unknown path: {path}'})}
452
+ body = event.get('body', '')
453
+ is_base64 = event.get('isBase64Encoded', False)
454
+ if is_base64:
455
+ import base64
456
+ body = base64.b64decode(body).decode('utf-8', errors='replace')
457
+ try:
458
+ otlp = json.loads(body) if isinstance(body, str) else body
459
+ except json.JSONDecodeError as e:
460
+ return {'statusCode': 400, 'body': json.dumps({'error': f'Invalid JSON: {e}'})}
461
+ bulk_lines = []
462
+ for rs in otlp.get('resourceSpans', []):
463
+ res_attrs = rs.get('resource', {}).get('attributes', [])
464
+ for ss in rs.get('scopeSpans', []):
465
+ scope_name = ss.get('scope', {}).get('name', '')
466
+ for span in ss.get('spans', []):
467
+ doc_id, doc = _convert_span(span, res_attrs, scope_name)
468
+ bulk_lines.append(json.dumps({'index': {'_index': 'otel-v1-apm-span-000001', '_id': doc_id}}))
469
+ bulk_lines.append(json.dumps(doc, default=str))
470
+ if not bulk_lines:
471
+ return {'statusCode': 200, 'body': json.dumps({'message': 'No spans to index'})}
472
+ bulk_body = '\n'.join(bulk_lines) + '\n'
473
+ status, resp_body = _signed_request('POST', '/otel-v1-apm-span-000001/_bulk', bulk_body)
474
+ return {'statusCode': status, 'body': resp_body}
475
+ Tags:
476
+ - Key: agent-health
477
+ Value: observability
478
+
479
+ OTLPIngestApi:
480
+ Type: AWS::ApiGatewayV2::Api
481
+ Properties:
482
+ Name: !Sub '${DomainName}-otlp-ingest'
483
+ ProtocolType: HTTP
484
+ CorsConfiguration:
485
+ AllowOrigins:
486
+ - '*'
487
+ AllowMethods:
488
+ - POST
489
+ AllowHeaders:
490
+ - content-type
491
+
492
+ OTLPIngestApiIntegration:
493
+ Type: AWS::ApiGatewayV2::Integration
494
+ Properties:
495
+ ApiId: !Ref OTLPIngestApi
496
+ IntegrationType: AWS_PROXY
497
+ IntegrationUri: !GetAtt OTLPIngestFunction.Arn
498
+ PayloadFormatVersion: '2.0'
499
+
500
+ OTLPIngestApiRoute:
501
+ Type: AWS::ApiGatewayV2::Route
502
+ Properties:
503
+ ApiId: !Ref OTLPIngestApi
504
+ RouteKey: 'POST /v1/{signal+}'
505
+ Target: !Sub 'integrations/${OTLPIngestApiIntegration}'
506
+
507
+ OTLPIngestApiStage:
508
+ Type: AWS::ApiGatewayV2::Stage
509
+ Properties:
510
+ ApiId: !Ref OTLPIngestApi
511
+ StageName: '$default'
512
+ AutoDeploy: true
513
+
514
+ OTLPIngestApiPermission:
515
+ Type: AWS::Lambda::Permission
516
+ Properties:
517
+ FunctionName: !Ref OTLPIngestFunction
518
+ Action: 'lambda:InvokeFunction'
519
+ Principal: apigateway.amazonaws.com
520
+ SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${OTLPIngestApi}/*'
521
+
522
+ # =============================================================================
523
+ # Outputs
524
+ # =============================================================================
525
+ Outputs:
526
+ OpenSearchEndpoint:
527
+ Description: OpenSearch domain endpoint URL
528
+ Value: !Sub 'https://${OpenSearchDomain.DomainEndpoint}'
529
+
530
+ OSISTraceIngestEndpoint:
531
+ Description: OSIS trace ingest endpoint (configure as OTEL_EXPORTER_OTLP_TRACES_ENDPOINT in your agent)
532
+ Value: !Join
533
+ - ''
534
+ - - 'https://'
535
+ - !Select [0, !GetAtt OSISTracePipeline.IngestEndpointUrls]
536
+
537
+ OSISLogsIngestEndpoint:
538
+ Description: OSIS logs ingest endpoint (configure as OTEL_EXPORTER_OTLP_LOGS_ENDPOINT in your agent)
539
+ Value: !Join
540
+ - ''
541
+ - - 'https://'
542
+ - !Select [0, !GetAtt OSISLogsPipeline.IngestEndpointUrls]
543
+
544
+ Region:
545
+ Description: AWS Region where the stack was deployed
546
+ Value: !Ref 'AWS::Region'
547
+
548
+ IngestionRoleArn:
549
+ Description: IAM role ARN for agents to assume when pushing telemetry via SigV4
550
+ Value: !GetAtt IngestionRole.Arn
551
+
552
+ OTLPIngestEndpoint:
553
+ Description: >-
554
+ HTTPS endpoint for OTLP trace/log ingestion (no SigV4 needed on client).
555
+ Set OTEL_EXPORTER_OTLP_ENDPOINT to this value in your agent config.
556
+ Value: !Sub 'https://${OTLPIngestApi}.execute-api.${AWS::Region}.amazonaws.com'
557
+
558
+ AgentHealthConfigJSON:
559
+ Description: >-
560
+ Copy this JSON block into your agent-health.config.json file,
561
+ or run: npx @opensearch-project/agent-health configure --from-stack AgentHealthObservability
562
+ Value: !Sub |
563
+ {
564
+ "observability": {
565
+ "endpoint": "https://${OpenSearchDomain.DomainEndpoint}",
566
+ "authType": "sigv4",
567
+ "awsRegion": "${AWS::Region}",
568
+ "awsService": "es",
569
+ "tlsSkipVerify": false
570
+ }
571
+ }