From 2307d0eb37748525ebc2e3bf54fa3bc046550a86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=AA=E0=A5=8D=E0=A4=B0=E0=A4=A5=E0=A4=AE=E0=A5=87?= =?UTF-8?q?=E0=A4=B6=20Sonpatki?= Date: Fri, 27 Dec 2024 12:30:29 +0530 Subject: [PATCH] Php core app changes (#40) * Make the app run via docker compose * Add basic DB monitoring * Refactoring --- php/core/7.3/Dockerfile | 7 +- php/core/7.3/composer.json | 4 +- php/core/7.3/docker-compose.yaml | 23 ++- php/core/7.3/index.php | 110 +++++++++--- php/core/7.3/instrumentation.php | 94 ---------- php/core/7.3/last9/instrumentHttpClient.php | 44 +++++ php/core/7.3/last9/instrumentPDO.php | 131 ++++++++++++++ php/core/7.3/last9/instrumentation.php | 183 ++++++++++++++++++++ 8 files changed, 472 insertions(+), 124 deletions(-) delete mode 100644 php/core/7.3/instrumentation.php create mode 100644 php/core/7.3/last9/instrumentHttpClient.php create mode 100644 php/core/7.3/last9/instrumentPDO.php create mode 100644 php/core/7.3/last9/instrumentation.php diff --git a/php/core/7.3/Dockerfile b/php/core/7.3/Dockerfile index a175f8d..562c24f 100644 --- a/php/core/7.3/Dockerfile +++ b/php/core/7.3/Dockerfile @@ -5,7 +5,9 @@ RUN apt-get update && apt-get install -y \ git \ unzip \ libzip-dev \ - && docker-php-ext-install zip + default-mysql-client \ + && docker-php-ext-install zip pdo pdo_mysql \ + && docker-php-ext-enable pdo_mysql # Install Composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer @@ -16,6 +18,9 @@ WORKDIR /var/www/html # Copy composer files first COPY composer.json ./ +# Copy the last9 directory +# COPY last9/ ./last9/ + # Debug: List contents after copying composer.json RUN ls -la && \ # Run composer install with verbose output diff --git a/php/core/7.3/composer.json b/php/core/7.3/composer.json index efe861e..7b25c1b 100644 --- a/php/core/7.3/composer.json +++ b/php/core/7.3/composer.json @@ -4,7 +4,7 @@ }, "autoload": { "psr-4": { - "App\\": "src/" + "Last9\\": "last9/" } } -} +} \ No newline at end of file diff --git a/php/core/7.3/docker-compose.yaml b/php/core/7.3/docker-compose.yaml index 1fbf27f..3ada7ba 100644 --- a/php/core/7.3/docker-compose.yaml +++ b/php/core/7.3/docker-compose.yaml @@ -12,4 +12,25 @@ services: - OTEL_EXPORTER_OTLP_ENDPOINT=${LAST_OTEL_EXPORTER_OTLP_ENDPOINT} - OTEL_SERVICE_NAME=my-demo-service - OTEL_DEPLOYMENT_ENVIRONMENT=production - - OTEL_LOG_LEVEL=debug \ No newline at end of file + - OTEL_LOG_LEVEL=debug + - DB_HOST=db + - DB_USER=diceuser + - DB_PASSWORD=dicepass + - DB_NAME=dicedb + depends_on: + - db + + db: + image: mariadb:10.5 + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: dicedb + MYSQL_USER: diceuser + MYSQL_PASSWORD: dicepass + volumes: + - db_data:/var/lib/mysql + ports: + - "3306:3306" + +volumes: + db_data: \ No newline at end of file diff --git a/php/core/7.3/index.php b/php/core/7.3/index.php index ef92d09..f3e6c58 100644 --- a/php/core/7.3/index.php +++ b/php/core/7.3/index.php @@ -1,34 +1,92 @@ exec("CREATE TABLE IF NOT EXISTS dice_rolls ( + id INT AUTO_INCREMENT PRIMARY KEY, + roll_value INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); +} catch (\Exception $e) { + error_log("Error creating table: " . $e->getMessage()); +} -// Get HTTP method and create operation name -$method = $_SERVER['REQUEST_METHOD']; -$operationName = $method . ' ' . $uri; +// Initialize instrumented HTTP client +$http = new \Last9\InstrumentedHttpClient([ + 'timeout' => 5, + 'connect_timeout' => 2, + 'verify' => false // Added to handle HTTPS issues +]); + +$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); -// Route handling -switch ($uri) { - case '/': - echo "Hello World!"; - // Instrument the HTTP request with path-based operation name - instrumentHTTPRequest($operationName, []); - break; +try { + switch ($uri) { + case '/': + echo "Hello World!"; + break; - case '/rolldice': - $result = random_int(1, 6); - - // Instrument the HTTP request with path-based operation name - instrumentHTTPRequest($operationName, ['result' => strval($result)]); - - echo "Rolled dice result: $result"; - break; + case '/rolldice': + $result = random_int(1, 6); + + // Insert new roll + $stmt = $pdo->prepare("INSERTT INTO dice_rolls (roll_value) VALUES (?)"); + $stmt->execute([$result]); + + // Get last 5 rolls with error logging + try { + $stmt = $pdo->prepare("SELECT roll_value, created_at FROM dice_rolls ORDER BY created_at DESC LIMIT 5"); + $stmt->execute(); + $previousRolls = $stmt->fetchAll(\PDO::FETCH_ASSOC); + // error_log("Previous rolls: " . print_r($previousRolls, true)); + } catch (\Exception $e) { + error_log("Error fetching previous rolls: " . $e->getMessage()); + $previousRolls = []; + } + + // Make external API call with error handling + try { + $response = $http->request('GET', "http://numbersapi.com/{$result}/math", [ + 'headers' => [ + 'Accept' => 'text/plain', + 'User-Agent' => 'PHP/1.0' + ], + 'verify' => false, // Disable SSL verification for testing + 'timeout' => 30 + ]); + $numberFact = $response->getBody(); + error_log("Number fact API response: " . $numberFact); + } catch (\Exception $e) { + error_log("Error fetching number fact: " . $e->getMessage() . "\n" . $e->getTraceAsString()); + $numberFact = "Could not fetch fact due to: " . $e->getMessage(); + } + + $response = [ + 'current_roll' => $result, + 'fact' => "test", + 'previous_rolls' => $previousRolls + ]; + +// error_log("Sending response: " . print_r($response, true)); + echo json_encode($response, JSON_PRETTY_PRINT); + break; - default: - header("HTTP/1.0 404 Not Found"); - instrumentHTTPRequest($operationName, ['result' => strval($result)]); - echo "404 Not Found"; - break; + default: + http_response_code(404); + echo "404 Not Found"; + break; + } +} catch (Exception $e) { + error_log("Main error: " . $e->getMessage()); + http_response_code(500); + echo "Error: " . $e->getMessage(); } \ No newline at end of file diff --git a/php/core/7.3/instrumentation.php b/php/core/7.3/instrumentation.php deleted file mode 100644 index e9aae82..0000000 --- a/php/core/7.3/instrumentation.php +++ /dev/null @@ -1,94 +0,0 @@ - [ - [ - "resource" => [ - "attributes" => [ - ["key" => "service.name", "value" => ["stringValue" => "demo-service"]], - ["key" => "deployment.environment", "value" => ["stringValue" => "production"]], - ["key" => "host.name", "value" => ["stringValue" => gethostname()]], - ["key" => "os.type", "value" => ["stringValue" => PHP_OS]], - ["key" => "process.runtime.name", "value" => ["stringValue" => "php"]], - ["key" => "process.runtime.version", "value" => ["stringValue" => PHP_VERSION]] - ] - ], - "scopeSpans" => [ - [ - "spans" => [ - [ - "traceId" => $traceId, - "spanId" => $spanId, - "name" => $operationName, - "kind" => 2, // Server = 2 as per OTEL spec - "startTimeUnixNano" => $timestamp * 1000, - "endTimeUnixNano" => ($timestamp + 1000) * 1000, - "attributes" => array_merge( - array_map(function ($key, $value) { - return ["key" => $key, "value" => ["stringValue" => (string)$value]]; - }, array_keys($attributes), $attributes), - [ - ["key" => "http.method", "value" => ["stringValue" => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN']], - ["key" => "http.target", "value" => ["stringValue" => $_SERVER['REQUEST_URI'] ?? '']], - ["key" => "http.host", "value" => ["stringValue" => $_SERVER['HTTP_HOST'] ?? '']], - ["key" => "http.scheme", "value" => ["stringValue" => isset($_SERVER['HTTPS']) ? 'https' : 'http']], - ["key" => "http.status_code", "value" => ["intValue" => http_response_code()]], - ["key" => "http.response_content_length", "value" => ["intValue" => isset($response) ? strlen($response) : 0]], - ["key" => "http.user_agent", "value" => ["stringValue" => $_SERVER['HTTP_USER_AGENT'] ?? '']], - ["key" => "net.peer.ip", "value" => ["stringValue" => $_SERVER['REMOTE_ADDR'] ?? '']] - ] - ), - "status" => [ - "code" => http_response_code() < 400 ? 1 : 2, // OK = 1, ERROR = 2 - "message" => http_response_code() >= 400 ? "HTTP " . http_response_code() : "" - ] - ] - ] - ] - ] - ] - ] - ]; - - try { - // Send the main response to the client first - if (function_exists('fastcgi_finish_request')) { - fastcgi_finish_request(); - } - - // Prevent the script from being terminated even if the client disconnects - ignore_user_abort(true); - - // Set unlimited time for the background process - set_time_limit(0); - - $client = new Client([ - 'timeout' => 5, // Add a timeout to prevent hanging - 'connect_timeout' => 2 - ]); - - $response = $client->post(OTLP_COLLECTOR_URL, [ - 'json' => $tracePayload, - 'headers' => [ - 'Content-Type' => 'application/json', - 'Authorization' => 'Basic $last9_otlp_header' - ] - ]); - - error_log("[OpenTelemetry] Trace sent successfully: " . $operationName); - return true; - } catch (\Exception $e) { - error_log("[OpenTelemetry] Failed to send trace: " . $e->getMessage()); - return false; - } -} \ No newline at end of file diff --git a/php/core/7.3/last9/instrumentHttpClient.php b/php/core/7.3/last9/instrumentHttpClient.php new file mode 100644 index 0000000..44f9402 --- /dev/null +++ b/php/core/7.3/last9/instrumentHttpClient.php @@ -0,0 +1,44 @@ +client = new Client($config); + } + + public function request($method, $uri, array $options = []) { + $spanData = \Last9\createSpan( + 'http.client', + \Last9\Instrumentation::getRootSpanId(), + [ + ['key' => 'http.method', 'value' => ['stringValue' => $method]], + ['key' => 'http.url', 'value' => ['stringValue' => $uri]], + ['key' => 'http.flavor', 'value' => ['stringValue' => '1.1']], + ['key' => 'network.protocol.name', 'value' => ['stringValue' => 'http']], + ['key' => 'network.protocol.version', 'value' => ['stringValue' => '1.1']] + ] + ); + + try { + $response = $this->client->request($method, $uri, $options); + \Last9\endSpan($spanData, + ['code' => 1], + [ + ['key' => 'http.status_code', 'value' => ['intValue' => $response->getStatusCode()]], + ['key' => 'http.response.body.size', 'value' => ['intValue' => strlen($response->getBody())]] + ] + ); + return $response; + } catch (\Exception $e) { + \Last9\endSpan($spanData, + ['code' => 2, 'message' => $e->getMessage()], + [['key' => 'error.message', 'value' => ['stringValue' => $e->getMessage()]]] + ); + throw $e; + } + } +} \ No newline at end of file diff --git a/php/core/7.3/last9/instrumentPDO.php b/php/core/7.3/last9/instrumentPDO.php new file mode 100644 index 0000000..558646d --- /dev/null +++ b/php/core/7.3/last9/instrumentPDO.php @@ -0,0 +1,131 @@ +dsn = $dsn; + $this->username = $username; + $this->password = $password; + $this->options = $options; + parent::__construct($dsn, $username, $password, array_merge([ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION + ], $options)); + } + + public function prepare($query, $options = []) { + try { + $stmt = parent::prepare($query, $options); + return new InstrumentedPDOStatement($stmt, $query); + } catch (\PDOException $e) { + // Create and end span here for prepare errors + $spanData = \Last9\createSpan( + 'database.query', + \Last9\Instrumentation::getRootSpanId(), + [ + ['key' => 'db.system', 'value' => ['stringValue' => 'mariadb']], + ['key' => 'db.statement', 'value' => ['stringValue' => $query]], + ['key' => 'db.type', 'value' => ['stringValue' => 'sql']], + ['key' => 'db.operation', 'value' => ['stringValue' => 'prepare']] + ] + ); + \Last9\endSpan($spanData, + ['code' => 2, 'message' => $e->getMessage()], + [ + ['key' => 'error.message', 'value' => ['stringValue' => $e->getMessage()]], + ['key' => 'error.code', 'value' => ['stringValue' => $e->getCode()]], + ['key' => 'error.type', 'value' => ['stringValue' => get_class($e)]] + ] + ); + throw $e; + } + } + + public function query(string $query, ?int $fetchMode = null, ...$fetchModeArgs) { + $spanData = \Last9\createSpan( + 'database.query', + \Last9\Instrumentation::getRootSpanId(), + [ + ['key' => 'db.system', 'value' => ['stringValue' => 'mariadb']], + ['key' => 'db.statement', 'value' => ['stringValue' => $query]], + ['key' => 'db.type', 'value' => ['stringValue' => 'sql']], + ['key' => 'db.operation', 'value' => ['stringValue' => 'query']] + ] + ); + + try { + $result = parent::query($query, $fetchMode, ...$fetchModeArgs); + \Last9\endSpan($spanData, ['code' => 1]); + return $result; + } catch (\PDOException $e) { + \Last9\endSpan($spanData, + ['code' => 2, 'message' => $e->getMessage()], + [ + ['key' => 'error.message', 'value' => ['stringValue' => $e->getMessage()]], + ['key' => 'error.code', 'value' => ['stringValue' => $e->getCode()]], + ['key' => 'error.type', 'value' => ['stringValue' => get_class($e)]] + ] + ); + throw $e; + } + } +} + +class InstrumentedPDOStatement { + private $statement; + private $query; + + public function __construct(\PDOStatement $statement, $query) { + $this->statement = $statement; + $this->query = $query; + } + + public function execute($params = null) { + $spanData = \Last9\createSpan( + 'database.query', + \Last9\Instrumentation::getRootSpanId(), + [ + ['key' => 'db.system', 'value' => ['stringValue' => 'mariadb']], + ['key' => 'db.statement', 'value' => ['stringValue' => $this->query]], + ['key' => 'db.type', 'value' => ['stringValue' => 'sql']], + ['key' => 'db.operation', 'value' => ['stringValue' => 'execute']], + ['key' => 'db.parameters', 'value' => ['stringValue' => json_encode($params)]] + ] + ); + + try { + $result = $this->statement->execute($params); + \Last9\endSpan($spanData, ['code' => 1]); + return $result; + } catch (\PDOException $e) { + \Last9\endSpan($spanData, + ['code' => 2, 'message' => $e->getMessage()], + [ + ['key' => 'error.message', 'value' => ['stringValue' => $e->getMessage()]], + ['key' => 'error.code', 'value' => ['stringValue' => $e->getCode()]], + ['key' => 'error.type', 'value' => ['stringValue' => get_class($e)]] + ] + ); + throw $e; + } + } + + public function __call($method, $args) { + return call_user_func_array([$this->statement, $method], $args); + } +} \ No newline at end of file diff --git a/php/core/7.3/last9/instrumentation.php b/php/core/7.3/last9/instrumentation.php new file mode 100644 index 0000000..482d278 --- /dev/null +++ b/php/core/7.3/last9/instrumentation.php @@ -0,0 +1,183 @@ + Instrumentation::$traceId, + 'spanId' => $spanId, + 'parentSpanId' => $parentSpanId, + 'name' => $name, + 'kind' => 2, // Server + 'startTimeUnixNano' => $timestamp * 1000, + 'endTimeUnixNano' => null, + 'attributes' => $attributes, + 'status' => ['code' => 1] + ]; + + return ['span' => $span, 'startTime' => $timestamp]; +} + +function endSpan(&$spanData, $status = ['code' => 1], $additionalAttributes = []) { + $endTime = (int)(microtime(true) * 1e6); + $spanData['span']['endTimeUnixNano'] = $endTime * 1000; + $spanData['span']['status'] = $status; + $spanData['span']['attributes'] = array_merge( + $spanData['span']['attributes'], + $additionalAttributes + ); + + Instrumentation::addSpan($spanData['span']); + return $spanData; +} + +class Instrumentation { + public static $traceId = null; + private static $spans = []; + private static $rootSpan = null; + + public static function getRootSpanId() { + return self::$rootSpan['span']['spanId'] ?? null; + } + + public static function addSpan($span) { + self::$spans[] = $span; + } + + public static function autoInit() { + if (self::$rootSpan === null) { + $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + $method = $_SERVER['REQUEST_METHOD']; + self::$traceId = bin2hex(random_bytes(16)); + self::$spans = []; + self::$rootSpan = self::createRootSpan("$method $uri"); + } + return self::$rootSpan; + } + + private static function createRootSpan($name) { + $spanData = createSpan($name); + + $httpAttributes = [ + ['key' => 'http.method', 'value' => ['stringValue' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN']], + ['key' => 'http.target', 'value' => ['stringValue' => $_SERVER['REQUEST_URI'] ?? '']], + ['key' => 'http.host', 'value' => ['stringValue' => $_SERVER['HTTP_HOST'] ?? '']], + ['key' => 'http.scheme', 'value' => ['stringValue' => isset($_SERVER['HTTPS']) ? 'https' : 'http']], + ['key' => 'http.status_code', 'value' => ['intValue' => http_response_code()]], + ['key' => 'http.user_agent', 'value' => ['stringValue' => $_SERVER['HTTP_USER_AGENT'] ?? '']], + ['key' => 'net.peer.ip', 'value' => ['stringValue' => $_SERVER['REMOTE_ADDR'] ?? '']] + ]; + + $spanData['span']['attributes'] = $httpAttributes; + return $spanData; + } + + public static function finish($status = ['code' => 1], $attributes = []) { + if (self::$rootSpan) { + endSpan(self::$rootSpan, $status, $attributes); + self::sendTraces(); + } + } + + private static function sendTraces() { + $tracePayload = [ + 'resourceSpans' => [ + [ + 'resource' => [ + 'attributes' => [ + ['key' => 'service.name', 'value' => ['stringValue' => 'demo-service']], + ['key' => 'deployment.environment', 'value' => ['stringValue' => 'production']], + ['key' => 'host.name', 'value' => ['stringValue' => gethostname()]], + ['key' => 'os.type', 'value' => ['stringValue' => PHP_OS]], + ['key' => 'process.runtime.name', 'value' => ['stringValue' => 'php']], + ['key' => 'process.runtime.version', 'value' => ['stringValue' => PHP_VERSION]] + ] + ], + 'scopeSpans' => [ + [ + 'spans' => self::$spans + ] + ] + ] + ] + ]; + + try { + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } + + ignore_user_abort(true); + set_time_limit(0); + + $client = new Client([ + 'timeout' => 5, + 'connect_timeout' => 2 + ]); + + $response = $client->post(OTLP_COLLECTOR_URL, [ + 'json' => $tracePayload, + 'headers' => [ + 'Content-Type' => 'application/json', + 'Authorization' => 'Basic $last9_otlp_header' + ] + ]); + + error_log("[OpenTelemetry] Traces sent successfully: " . count(self::$spans) . " spans"); + return true; + } catch (\Exception $e) { + error_log("[OpenTelemetry] Failed to send traces: " . $e->getMessage()); + return false; + } + } +} + +// Auto-initialize instrumentation +Instrumentation::autoInit(); + +// Register shutdown function +// Register shutdown function +register_shutdown_function(function() { + $error = error_get_last(); + $httpCode = http_response_code(); + + if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { + // PHP Error occurred + Instrumentation::finish( + ['code' => 2, 'message' => $error['message']], + [['key' => 'error.message', 'value' => ['stringValue' => $error['message']]]] + ); + } elseif ($httpCode >= 400) { + // HTTP error occurred (4xx or 5xx) + Instrumentation::finish( + ['code' => 2, 'message' => 'HTTP ' . $httpCode], + [ + ['key' => 'error.type', 'value' => ['stringValue' => 'HTTPError']], + ['key' => 'http.status_code', 'value' => ['intValue' => $httpCode]] + ] + ); + } else { + // Success case + Instrumentation::finish( + ['code' => 1], + [['key' => 'http.status_code', 'value' => ['intValue' => $httpCode]]] + ); + } +}); \ No newline at end of file