diff --git a/observability-pocs/poc_0_secret_manager.php b/observability-pocs/poc_0_secret_manager.php new file mode 100644 index 000000000000..5fd2caaf3b84 --- /dev/null +++ b/observability-pocs/poc_0_secret_manager.php @@ -0,0 +1,123 @@ +tracerProvider = $tracerProvider; + + if (TelemetryConfiguration::isTracingEnabled($tracerProvider)) { + $provider = $this->tracerProvider ?? \OpenTelemetry\API\Globals::tracerProvider(); + $this->tracer = $provider->getTracer('google-cloud-secretmanager', '1.0.0'); + } else { + $this->tracer = null; + } + } + + public function accessSecretVersion(string $name) { + $span = null; + if ($this->tracer) { + $span = $this->tracer->spanBuilder('SecretManagerServiceClient.access_secret_version') + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->setAttribute('gcp.client.service', 'secretmanager') + ->startSpan(); + $scope = $span->activate(); + } + + try { + if ($span) { + $t4 = $this->tracer->spanBuilder('GET /v1/' . $name . ':access') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttribute('rpc.system.name', 'http') + ->setAttribute('http.request.method', 'GET') + ->setAttribute('gcp.resource.destination.id', '//secretmanager.googleapis.com/' . $name) + ->startSpan(); + + $t4->setAttribute('http.response.status_code', 200); + $t4->end(); + } + + return "super_secret_value"; + } finally { + if ($span) { + $span->end(); + $scope->detach(); + } + } + } +} + +use OpenTelemetry\SDK\Trace\SpanExporter\InMemoryExporter; +use OpenTelemetry\SDK\Trace\TracerProvider; + +// Setup OTel SDK for testing +$exporter = new InMemoryExporter(); +$tracerProvider = new TracerProvider(new SimpleSpanProcessor($exporter)); + +echo "--- Test 1: Disabled by default ---\n"; +putenv('GOOGLE_CLOUD_TELEMETRY_ENABLED'); // Unset +$client1 = new SecretManagerClientMock(); +$client1->accessSecretVersion('projects/my-project/secrets/my-secret'); +echo "Spans generated: " . count($exporter->getSpans()) . "\n\n"; + +echo "--- Test 2: Enabled via Constructor ---\n"; +$exporter2 = new InMemoryExporter(); +$tracerProvider2 = new TracerProvider(new SimpleSpanProcessor($exporter2)); +$client2 = new SecretManagerClientMock($tracerProvider2); +$client2->accessSecretVersion('projects/my-project/secrets/my-secret'); +echo "Spans generated: " . count($exporter2->getSpans()) . "\n\n"; + +echo "--- Test 3: Global Override (Disabled) ---\n"; +putenv('GOOGLE_CLOUD_TELEMETRY_ENABLED=false'); +$exporter3 = new InMemoryExporter(); +$tracerProvider3 = new TracerProvider(new SimpleSpanProcessor($exporter3)); +$client3 = new SecretManagerClientMock($tracerProvider3); +$client3->accessSecretVersion('projects/my-project/secrets/my-secret'); +echo "Spans generated: " . count($exporter3->getSpans()) . "\n\n"; + +echo "--- Test 4: Environment Enablement (Global Provider) ---\n"; +putenv('GOOGLE_CLOUD_TELEMETRY_ENABLED=true'); +$exporter4 = new InMemoryExporter(); +$tracerProvider4 = new TracerProvider(new SimpleSpanProcessor($exporter4)); +\OpenTelemetry\API\Globals::registerInitializer(function(\OpenTelemetry\API\Instrumentation\Configurator $configurator) use ($tracerProvider4) { + return $configurator->withTracerProvider($tracerProvider4); +}); +$client4 = new SecretManagerClientMock(); +$client4->accessSecretVersion('projects/my-project/secrets/my-secret'); +echo "Spans generated: " . count($exporter4->getSpans()) . "\n"; + + diff --git a/observability-pocs/poc_1_pubsub_streaming.php b/observability-pocs/poc_1_pubsub_streaming.php new file mode 100644 index 000000000000..58b7beba5f4d --- /dev/null +++ b/observability-pocs/poc_1_pubsub_streaming.php @@ -0,0 +1,59 @@ +getTracer('google-cloud-pubsub-mock', '1.0.0'); + +// Mock Streaming Pull Loop +echo "Starting Pub/Sub Streaming Pull PoC...\n"; + +// The overall subscriber span (T2) +$subscriberSpan = $tracer->spanBuilder('Subscriber.StreamingPull') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->startSpan(); +$subscriberScope = $subscriberSpan->activate(); + +try { + for ($i = 1; $i <= 3; $i++) { + // T3: Process individual message + $messageSpan = $tracer->spanBuilder("ProcessMessage $i") + ->setSpanKind(SpanKind::KIND_INTERNAL) + ->startSpan(); + + $messageScope = $messageSpan->activate(); + try { + // Simulate user callback executing + echo "Processing message $i in trace " . $messageSpan->getContext()->getTraceId() . "\n"; + + // T4: Simulated network call inside the user callback (e.g. acking the message) + $ackSpan = $tracer->spanBuilder("POST /v1/projects/p/subscriptions/s:acknowledge") + ->setSpanKind(SpanKind::KIND_CLIENT) + ->startSpan(); + $ackScope = $ackSpan->activate(); + try { + // Network delay + } finally { + $ackSpan->end(); + $ackScope->detach(); + } + } finally { + $messageSpan->end(); + $messageScope->detach(); // CRITICAL: Detaching prevents context leak to next message loop + } + } +} finally { + $subscriberSpan->end(); + $subscriberScope->detach(); +} + +echo "\nTotal Spans Exported: " . count($exporter->getSpans()) . "\n"; +foreach ($exporter->getSpans() as $span) { + echo "- Span: " . $span->getName() . " | Parent ID: " . $span->getParentContext()->getSpanId() . "\n"; +} diff --git a/observability-pocs/poc_2_gapic_resource_name.php b/observability-pocs/poc_2_gapic_resource_name.php new file mode 100644 index 000000000000..2e26fceb0066 --- /dev/null +++ b/observability-pocs/poc_2_gapic_resource_name.php @@ -0,0 +1,66 @@ + 'pubsub.googleapis.com', + 'http_annotation' => 'v1/{name=projects/*/topics/*}', + 'runtime_path' => 'v1/projects/my-project/topics/my-topic' + ], + [ + 'api_domain' => 'spanner.googleapis.com', + 'http_annotation' => 'v1/{session=projects/*/instances/*/databases/*/sessions/*}:commit', + 'runtime_path' => 'v1/projects/p/instances/i/databases/d/sessions/s:commit' + ], + [ + 'api_domain' => 'storage.googleapis.com', + 'http_annotation' => 'b/{bucket}/o/{object}', + 'runtime_path' => 'b/my-bucket/o/my-file.txt' + ] +]; + +echo "Starting GAPIC Resource Name Parser PoC...\n\n"; + +foreach ($tests as $test) { + echo "Testing annotation: " . $test['http_annotation'] . "\n"; + + // In gapic-generator-php, this regex generation would happen at compile-time. + // We convert the annotation to a regex pattern capturing the dynamic parts. + // Simplified regex generation for PoC: + $pattern = preg_replace('/\{([a-zA-Z0-9_]+)=([^\}]+)\}/', '(?P<$1>$2)', $test['http_annotation']); + // Handle simple {bucket} cases without explicit wildcards + $pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[^/:]+)', $pattern); + + // Replace * with regex wildcard for the generated pattern + $pattern = str_replace('*', '[^/:]+', $pattern); + $pattern = '#^' . $pattern . '$#'; + + echo " Generated Regex: $pattern\n"; + + // At runtime in the client library, we apply the regex to the actual path. + if (preg_match($pattern, $test['runtime_path'], $matches)) { + // Find the named capturing group that matches a valid GCP resource structure + // (For simplicity in PoC, we just take the first named match that contains a slash) + $extractedResource = null; + foreach ($matches as $key => $value) { + if (is_string($key) && strpos($value, '/') !== false) { + $extractedResource = $value; + break; + } + } + + if (!$extractedResource && isset($matches[1])) { + // Fallback for simple non-slashed variables (like bucket name) + $extractedResource = $matches['bucket'] ?? null; + } + + if ($extractedResource) { + $formattedId = '//' . $test['api_domain'] . '/' . $extractedResource; + echo " Extracted Resource ID: " . $formattedId . "\n\n"; + } else { + echo " FAILED to extract resource.\n\n"; + } + } else { + echo " FAILED to match runtime path.\n\n"; + } +} diff --git a/observability-pocs/poc_3_metric_exemplars.php b/observability-pocs/poc_3_metric_exemplars.php new file mode 100644 index 000000000000..83c8dd5aeaf6 --- /dev/null +++ b/observability-pocs/poc_3_metric_exemplars.php @@ -0,0 +1,61 @@ +getTracer('poc-tracer'); + +$metricExporter = new InMemoryMetricExporter(); +$meterProvider = OpenTelemetry\SDK\Metrics\MeterProvider::builder() + ->addReader(new ExportingReader($metricExporter)) + ->build(); +$meter = $meterProvider->getMeter('poc-meter'); + +$histogram = $meter->createHistogram( + 'gcp.client.request.duration', + 'ms', + 'Duration of client request' +); + +echo "Starting Metric Exemplars PoC...\n"; + +// Start a trace +$span = $tracer->spanBuilder('Storage.Upload')->setSpanKind(SpanKind::KIND_CLIENT)->startSpan(); +$scope = $span->activate(); + +try { + echo "Active Trace ID: " . $span->getContext()->getTraceId() . "\n"; + echo "Active Span ID: " . $span->getContext()->getSpanId() . "\n"; + + // Simulate workload + usleep(150000); // 150ms + + // Record the metric WHILE the trace context is active. + // The OTel SDK should automatically attach the active trace as an exemplar. + $histogram->record(150.5, ['gcp.client.service' => 'storage']); + echo "Recorded metric: 150.5 ms\n"; +} finally { + $span->end(); + $scope->detach(); +} + +// Force flush the metrics reader to the memory exporter +$meterProvider->shutdown(); + +echo "\nVerifying Metric Export for Exemplars:\n"; +$metrics = $metricExporter->collect(); + +foreach ($metrics as $metric) { + if ($metric->name === 'gcp.client.request.duration') { + echo print_r($metric->data, true); + } +}