Skip to content

Commit a6c02e5

Browse files
committed
Added init AWSLambdaWrapper code
1 parent 8dda467 commit a6c02e5

File tree

3 files changed

+230
-1
lines changed

3 files changed

+230
-1
lines changed

src/Aws/composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
"prefer-stable": true,
1515
"require": {
1616
"php": "^8.1",
17+
"bref/bref": "^2.4",
1718
"open-telemetry/api": "^1.0",
1819
"open-telemetry/sdk": "^1.0",
20+
"open-telemetry/exporter-otlp": "^1.3",
21+
"open-telemetry/sem-conv": "^1.32",
1922
"aws/aws-sdk-php": "^3.232"
2023
},
2124
"require-dev": {
@@ -45,8 +48,9 @@
4548
"sort-packages": true,
4649
"allow-plugins": {
4750
"composer/package-versions-deprecated": true,
51+
"php-http/discovery": false,
4852
"symfony/runtime": true,
49-
"php-http/discovery": false
53+
"tbachert/spi": true
5054
}
5155
}
5256
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Aws\Lambda;
6+
7+
use Bref\Context\Context;
8+
use Bref\Event\Http\HttpRequestEvent;
9+
use OpenTelemetry\API\Trace\SpanKind;
10+
use OpenTelemetry\API\Trace\TracerInterface;
11+
use OpenTelemetry\Contrib\Aws\Lambda\Detector as LambdaDetector;
12+
use OpenTelemetry\Contrib\Aws\Xray\IdGenerator;
13+
use OpenTelemetry\Contrib\Aws\Xray\Propagator as XrayPropagator;
14+
use OpenTelemetry\Contrib\Otlp\OtlpHttpTransportFactory;
15+
use OpenTelemetry\Contrib\Otlp\SpanExporter as OtlpExporter;
16+
use OpenTelemetry\SDK\Common\Attribute\Attributes;
17+
use OpenTelemetry\SDK\Resource\ResourceInfo;
18+
use OpenTelemetry\SDK\Resource\ResourceInfoFactory;
19+
use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor;
20+
use OpenTelemetry\SDK\Trace\TracerProvider;
21+
use OpenTelemetry\SemConv\TraceAttributes;
22+
23+
class AwsLambdaWrapper
24+
{
25+
private const LAMBDA_NAME_ENV = 'AWS_LAMBDA_FUNCTION_NAME';
26+
private const LAMBDA_INVOCATION_CONTEXT_ENV = 'LAMBDA_INVOCATION_CONTEXT';
27+
private const OTEL_SERVICE_NAME_ENV = 'OTEL_SERVICE_NAME';
28+
private const OTEL_EXPORTER_ENDPOINT_ENV = 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT';
29+
30+
private const DEFAULT_OTLP_EXPORTER_ENDPOINT = 'http://localhost:4318/v1/traces';
31+
32+
private TracerInterface $tracer;
33+
private bool $isColdStart = true;
34+
35+
private static ?AwsLambdaWrapper $instance = null;
36+
37+
/** @psalm-suppress PossiblyUnusedMethod */
38+
public static function getInstance(): AwsLambdaWrapper
39+
{
40+
if (self::$instance === null) {
41+
self::$instance = new AwsLambdaWrapper();
42+
}
43+
44+
return self::$instance;
45+
}
46+
47+
private function __construct()
48+
{
49+
$lambdaDetector = new LambdaDetector();
50+
$defaultResource = ResourceInfoFactory::defaultResource();
51+
$resource = $defaultResource->merge($lambdaDetector->getResource());
52+
53+
if (getenv(self::OTEL_SERVICE_NAME_ENV) == null) {
54+
$service_name_resource = ResourceInfo::create(Attributes::create([
55+
TraceAttributes::SERVICE_NAME => getenv(self::LAMBDA_NAME_ENV),
56+
]));
57+
$resource = $resource->merge($service_name_resource);
58+
}
59+
60+
// 1) Configure OTLP/HTTP exporter
61+
$transportFactory = new OtlpHttpTransportFactory();
62+
$transport = $transportFactory->create(
63+
getenv(self::OTEL_EXPORTER_ENDPOINT_ENV)
64+
?: self::DEFAULT_OTLP_EXPORTER_ENDPOINT,
65+
'application/x-protobuf'
66+
);
67+
$exporter = new OtlpExporter($transport);
68+
69+
$spanProcessor = new SimpleSpanProcessor($exporter);
70+
$xrayIdGenerator = new IdGenerator();
71+
$tracerProvider = new TracerProvider($spanProcessor, null, $resource, null, $xrayIdGenerator);
72+
73+
$this->tracer = $tracerProvider->getTracer('php-lambda');
74+
75+
register_shutdown_function(function () use ($tracerProvider): void {
76+
$tracerProvider->shutdown();
77+
});
78+
79+
}
80+
81+
/** @psalm-suppress PossiblyUnusedMethod */
82+
public function getTracer(): TracerInterface
83+
{
84+
return $this->tracer;
85+
}
86+
87+
/** @psalm-suppress PossiblyUnusedMethod */
88+
public function setTracer(TracerInterface $newTracer): void
89+
{
90+
$this->tracer = $newTracer;
91+
}
92+
93+
/** @psalm-suppress PossiblyUnusedMethod
94+
* @psalm-suppress ArgumentTypeCoercion
95+
* @psalm-suppress PossiblyFalseArgument
96+
*/
97+
public function WrapHandler(callable $handler): callable
98+
{
99+
return function (array $event, Context $context) use ($handler): array {
100+
101+
$jsonContext = getenv(self::LAMBDA_INVOCATION_CONTEXT_ENV) ?: '';
102+
103+
$lambdaContext = json_decode($jsonContext, true, 512, JSON_THROW_ON_ERROR);
104+
105+
$awsRequestId = $lambdaContext['awsRequestId'] ?? null;
106+
$invokedFunctionArn = $lambdaContext['invokedFunctionArn'] ?? null;
107+
$traceId = $lambdaContext['traceId'] ?? null;
108+
109+
$propagator = new XrayPropagator();
110+
$carrier = [
111+
XrayPropagator::AWSXRAY_TRACE_ID_HEADER => $traceId,
112+
];
113+
$parentCtx = $propagator->extract($carrier);
114+
115+
$lambdaName = getenv(self::LAMBDA_NAME_ENV);
116+
117+
$lambdaSpanAttributes = Attributes::create([
118+
TraceAttributes::FAAS_TRIGGER => self::isHttpRequest($event) ? 'http' : 'other',
119+
TraceAttributes::FAAS_COLDSTART => $this->isColdStart,
120+
TraceAttributes::FAAS_NAME => $lambdaName,
121+
TraceAttributes::FAAS_INVOCATION_ID => $awsRequestId,
122+
TraceAttributes::CLOUD_RESOURCE_ID => self::getCloudResourceId($invokedFunctionArn),
123+
TraceAttributes::CLOUD_ACCOUNT_ID => self::getAccountId($invokedFunctionArn),
124+
]);
125+
126+
$this->isColdStart = false;
127+
128+
// Start a root span for the Lambda invocation
129+
$rootSpan = $this->tracer
130+
->spanBuilder($lambdaName)
131+
->setParent($parentCtx)
132+
->setAttributes($lambdaSpanAttributes)
133+
->setSpanKind(SpanKind::KIND_SERVER)
134+
->startSpan();
135+
$rootScope = $rootSpan->activate();
136+
137+
try {
138+
return $handler($event, $context);
139+
} finally {
140+
$rootSpan->end();
141+
$rootScope->detach();
142+
}
143+
};
144+
}
145+
146+
private static function getAccountId(?string $functionArn): ?string
147+
{
148+
if (empty($functionArn)) {
149+
return null;
150+
}
151+
152+
$segments = explode(':', $functionArn);
153+
154+
return isset($segments[4]) ? $segments[4] : null;
155+
}
156+
157+
private static function getCloudResourceId(?string $functionArn): ?string
158+
{
159+
if (empty($functionArn)) {
160+
return null;
161+
}
162+
163+
// According to cloud.resource_id description https://github.com/open-telemetry/semantic-conventions/blob/v1.32.0/docs/resource/faas.md?plain=1#L59-L63
164+
// the 8th part of arn (function version or alias, see https://docs.aws.amazon.com/lambda/latest/dg/lambda-api-permissions-ref.html)
165+
// should not be included into cloud.resource_id
166+
$segments = explode(':', $functionArn);
167+
if (count($segments) >= 8) {
168+
return implode(':', array_slice($segments, 0, 7));
169+
}
170+
171+
return $functionArn;
172+
}
173+
174+
private static function isHttpRequest(array $event): bool
175+
{
176+
try {
177+
/** @phan-suppress-next-line PhanNoopNew */
178+
new HttpRequestEvent($event);
179+
} catch (\Throwable $th) {
180+
return false;
181+
}
182+
183+
return true;
184+
}
185+
186+
}

src/Aws/src/Lambda/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# AWS Lambda Instrumentation for OpenTelemetry PHP
2+
This package supports manual instrumentation for the AWS Lambda functions written in PHP. PHP Lambda functions can be deployed using [Bref](https://bref.sh/) which this package primary depends on when doing instrumentation.
3+
4+
5+
## Using the AWS Lambda Instrumentation with AWS Lambda Functions
6+
Below is a example on how to setup AWS Lambda Instrumentation:
7+
8+
1. Follow the steps in this [README](../../README.md) to install the AWS Contrib Depedency.
9+
2. Follw the example below:
10+
```php
11+
<?php
12+
// index.php
13+
14+
require __DIR__ . '/vendor/autoload.php';
15+
16+
use Bref\Context\Context;
17+
use OpenTelemetry\Contrib\Aws\Lambda\AwsLambdaWrapper;
18+
19+
// Get AwsLambdaWrapper Instance which already constructs a Tracer to be used for creating spans
20+
$wrapper = AwsLambdaWrapper::getInstance();
21+
22+
// Use the default tracer created by the wrapper to manually create and instrument other spans.
23+
$tracer = $wrapper->getTracer();
24+
25+
// Alternatively, you can create your own tracer and then call $wrapper->setTracer($customTracer)
26+
27+
// Your PHP Handler Function and logic.
28+
$handlerFunction = function (array $event, Context $context) use ($tracer): array {
29+
// .... handler code using $tracer to manually instrument spans.
30+
return [
31+
'statusCode' => 404,
32+
'headers' => ['Content-Type' => 'text/plain'],
33+
'body' => 'Not Found',
34+
];
35+
};
36+
37+
// The WrapHandler Function is where you pass the original function and it gets instrumented
38+
return $wrapper->WrapHandler($handlerFunction);
39+
```

0 commit comments

Comments
 (0)