Merge pull request #3656 from appwrite/feat-functions-console-logs
Functions Console Logs Capture
This commit is contained in:
commit
69a4f51d58
12 changed files with 103 additions and 27 deletions
|
@ -2409,6 +2409,17 @@ $collections = [
|
|||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'stdout',
|
||||
'type' => Database::VAR_STRING,
|
||||
'format' => '',
|
||||
'size' => 1000000,
|
||||
'signed' => true,
|
||||
'required' => false,
|
||||
'default' => null,
|
||||
'array' => false,
|
||||
'filters' => [],
|
||||
],
|
||||
[
|
||||
'$id' => 'statusCode',
|
||||
'type' => Database::VAR_INTEGER,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
use Utopia\App;
|
||||
use Appwrite\Runtimes\Runtimes;
|
||||
|
||||
$runtimes = new Runtimes('v1');
|
||||
$runtimes = new Runtimes('v2');
|
||||
|
||||
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
|
||||
|
||||
|
|
|
@ -945,6 +945,7 @@ App::post('/v1/functions/:functionId/executions')
|
|||
$execution->setAttribute('status', $executionResponse['status']);
|
||||
$execution->setAttribute('statusCode', $executionResponse['statusCode']);
|
||||
$execution->setAttribute('response', $executionResponse['response']);
|
||||
$execution->setAttribute('stdout', $executionResponse['stdout']);
|
||||
$execution->setAttribute('stderr', $executionResponse['stderr']);
|
||||
$execution->setAttribute('time', $executionResponse['time']);
|
||||
} catch (\Throwable $th) {
|
||||
|
@ -965,6 +966,14 @@ App::post('/v1/functions/:functionId/executions')
|
|||
->setParam('functionStatus', $execution->getAttribute('status', ''))
|
||||
->setParam('functionExecutionTime', $execution->getAttribute('time') * 1000); // ms
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
if (!$isPrivilegedUser && !$isAppUser) {
|
||||
$execution->setAttribute('stdout', '');
|
||||
$execution->setAttribute('stderr', '');
|
||||
}
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_CREATED)
|
||||
->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
|
@ -1016,6 +1025,17 @@ App::get('/v1/functions/:functionId/executions')
|
|||
$results = $dbForProject->find('executions', $queries, $limit, $offset, [], [Database::ORDER_DESC], $cursorExecution ?? null, $cursorDirection);
|
||||
$total = $dbForProject->count('executions', $queries, APP_LIMIT_COUNT);
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
if (!$isPrivilegedUser && !$isAppUser) {
|
||||
$results = array_map(function ($execution) {
|
||||
$execution->setAttribute('stdout', '');
|
||||
$execution->setAttribute('stderr', '');
|
||||
return $execution;
|
||||
}, $results);
|
||||
}
|
||||
|
||||
$response->dynamic(new Document([
|
||||
'executions' => $results,
|
||||
'total' => $total,
|
||||
|
@ -1055,6 +1075,14 @@ App::get('/v1/functions/:functionId/executions/:executionId')
|
|||
throw new Exception(Exception::EXECUTION_NOT_FOUND);
|
||||
}
|
||||
|
||||
$roles = Authorization::getRoles();
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser($roles);
|
||||
$isAppUser = Auth::isAppUser($roles);
|
||||
if (!$isPrivilegedUser && !$isAppUser) {
|
||||
$execution->setAttribute('stdout', '');
|
||||
$execution->setAttribute('stderr', '');
|
||||
}
|
||||
|
||||
$response->dynamic($execution, Response::MODEL_EXECUTION);
|
||||
});
|
||||
|
||||
|
|
|
@ -488,6 +488,7 @@ App::post('/v1/execution')
|
|||
$executionStart = \microtime(true);
|
||||
$stdout = '';
|
||||
$stderr = '';
|
||||
$res = '';
|
||||
$statusCode = 0;
|
||||
$errNo = -1;
|
||||
$executorResponse = '';
|
||||
|
@ -515,6 +516,7 @@ App::post('/v1/execution')
|
|||
]);
|
||||
|
||||
$executorResponse = \curl_exec($ch);
|
||||
$executorResponse = json_decode($executorResponse, true);
|
||||
|
||||
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
|
@ -538,13 +540,19 @@ App::post('/v1/execution')
|
|||
|
||||
switch (true) {
|
||||
case $statusCode >= 500:
|
||||
$stderr = $executorResponse ?? 'Internal Runtime error.';
|
||||
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Internal Runtime error.';
|
||||
$stdout = ($executorResponse ?? [])['stdout'] ?? 'Internal Runtime error.';
|
||||
break;
|
||||
case $statusCode >= 100:
|
||||
$stdout = $executorResponse;
|
||||
$stdout = $executorResponse['stdout'];
|
||||
$res = $executorResponse['response'];
|
||||
if (is_array($res)) {
|
||||
$res = json_encode($res, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$stderr = $executorResponse ?? 'Execution failed.';
|
||||
$stderr = ($executorResponse ?? [])['stderr'] ?? 'Execution failed.';
|
||||
$stdout = ($executorResponse ?? [])['stdout'] ?? '';
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -557,7 +565,8 @@ App::post('/v1/execution')
|
|||
$execution = [
|
||||
'status' => $functionStatus,
|
||||
'statusCode' => $statusCode,
|
||||
'response' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
|
||||
'response' => \mb_strcut($res, 0, 1000000), // Limit to 1MB
|
||||
'stdout' => \mb_strcut($stdout, 0, 1000000), // Limit to 1MB
|
||||
'stderr' => \mb_strcut($stderr, 0, 1000000), // Limit to 1MB
|
||||
'time' => $executionTime,
|
||||
];
|
||||
|
@ -648,7 +657,7 @@ $http->on('start', function ($http) {
|
|||
/**
|
||||
* Warmup: make sure images are ready to run fast 🚀
|
||||
*/
|
||||
$runtimes = new Runtimes('v1');
|
||||
$runtimes = new Runtimes('v2');
|
||||
$allowList = empty(App::getEnv('_APP_FUNCTIONS_RUNTIMES')) ? [] : \explode(',', App::getEnv('_APP_FUNCTIONS_RUNTIMES'));
|
||||
$runtimes = $runtimes->getAll(true, $allowList);
|
||||
foreach ($runtimes as $runtime) {
|
||||
|
|
|
@ -392,10 +392,10 @@ sort($patterns);
|
|||
<tr>
|
||||
<th width="30"></th>
|
||||
<th width="160">Created</th>
|
||||
<th width="150">Status</th>
|
||||
<th width="120">Trigger</th>
|
||||
<th width="80">Runtime</th>
|
||||
<th></th>
|
||||
<th width="100">Status</th>
|
||||
<th width="80">Trigger</th>
|
||||
<th width="60">Runtime</th>
|
||||
<th width=""></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-ls-loop="project-function-executions.executions" data-ls-as="execution">
|
||||
|
@ -416,29 +416,44 @@ sort($patterns);
|
|||
<td data-title="Trigger: ">
|
||||
<span data-ls-bind="{{execution.trigger}}"></span>
|
||||
</td>
|
||||
<td data-title="Runtime: ">
|
||||
<td data-title="Time: ">
|
||||
<span data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-ls-bind="{{execution.time|seconds2hum}}"></span>
|
||||
<span data-ls-if="{{execution.status}} === 'waiting' || {{execution.status}} === 'processing'">-</span>
|
||||
</td>
|
||||
<td data-title="">
|
||||
<div data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-title="">
|
||||
<div data-ls-if="{{execution.status}} === 'completed' || {{execution.status}} === 'failed'" data-title="" style="display: flex;">
|
||||
<button class="desktops-only pull-end link margin-start text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Stderr</button>
|
||||
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Stdout</button>
|
||||
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-response-{{execution.$id}}">Response</button>
|
||||
|
||||
<button class="desktops-only pull-end link margin-start text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Errors</button>
|
||||
<button class="desktops-only pull-end link margin-start" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Output</button>
|
||||
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-response-{{execution.$id}}">Response</button>
|
||||
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Stdout</button>
|
||||
<button class="phones-only-inline tablets-only-inline link text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Stderr</button>
|
||||
|
||||
<button class="phones-only-inline tablets-only-inline link margin-end-small" data-ls-ui-trigger="execution-stdout-{{execution.$id}}">Output</button>
|
||||
<button class="phones-only-inline tablets-only-inline link text-danger" data-ls-ui-trigger="execution-stderr-{{execution.$id}}">Errors</button>
|
||||
|
||||
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-stdout-{{execution.$id}}">
|
||||
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-response-{{execution.$id}}">
|
||||
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
|
||||
|
||||
<h1>STDOUT</h1>
|
||||
<h1>RESPONSE</h1>
|
||||
|
||||
<div class="margin-bottom ide" data-ls-if="({{execution.response.length}})">
|
||||
<pre data-ls-bind="{{execution.response}}"></pre>
|
||||
</div>
|
||||
|
||||
<div class="margin-bottom" data-ls-if="(!{{execution.response.length}})">
|
||||
<p>No Response was logged.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-ui-modal class="modal width-large box close" data-button-alias="none" data-open-event="execution-stdout-{{execution.$id}}">
|
||||
<button type="button" class="close pull-end" data-ui-modal-close=""><i class="icon-cancel"></i></button>
|
||||
|
||||
<h1>STDOUT</h1>
|
||||
|
||||
<div class="margin-bottom ide" data-ls-if="({{execution.stdout.length}})">
|
||||
<pre data-ls-bind="{{execution.stdout}}"></pre>
|
||||
</div>
|
||||
|
||||
<div class="margin-bottom" data-ls-if="(!{{execution.stdout.length}})">
|
||||
<p>No output was logged.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -293,6 +293,7 @@ class FunctionsV1 extends Worker
|
|||
->setAttribute('status', $executionResponse['status'])
|
||||
->setAttribute('statusCode', $executionResponse['statusCode'])
|
||||
->setAttribute('response', $executionResponse['response'])
|
||||
->setAttribute('stdout', $executionResponse['stdout'])
|
||||
->setAttribute('stderr', $executionResponse['stderr'])
|
||||
->setAttribute('time', $executionResponse['time']);
|
||||
} catch (\Throwable $th) {
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"ext-zlib": "*",
|
||||
"ext-sockets": "*",
|
||||
"appwrite/php-clamav": "1.1.*",
|
||||
"appwrite/php-runtimes": "0.10.*",
|
||||
"appwrite/php-runtimes": "0.11.*",
|
||||
"utopia-php/framework": "0.20.*",
|
||||
"utopia-php/logger": "0.3.*",
|
||||
"utopia-php/abuse": "0.7.*",
|
||||
|
|
8
composer.lock
generated
8
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "993486075710ab0cdbba6c33f0b09218",
|
||||
"content-hash": "bbe0a04899feee1909b5924a2131dbee",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/jwt",
|
||||
|
@ -115,11 +115,11 @@
|
|||
},
|
||||
{
|
||||
"name": "appwrite/php-runtimes",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/appwrite/runtimes.git",
|
||||
"reference": "09874846c6bdb7be58c97b12323d2b35ec995409"
|
||||
"reference": "547fc026e11c0946846a8ac690898f5bf53be101"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0",
|
||||
|
@ -154,7 +154,7 @@
|
|||
"php",
|
||||
"runtimes"
|
||||
],
|
||||
"time": "2022-06-28T05:26:20+00:00"
|
||||
"time": "2022-08-15T14:03:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "chillerlan/php-qrcode",
|
||||
|
|
|
@ -65,9 +65,15 @@ class Execution extends Model
|
|||
'default' => '',
|
||||
'example' => '',
|
||||
])
|
||||
->addRule('stdout', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The script stdout output string. Logs the last 4,000 characters of the execution stdout output. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
|
||||
'default' => '',
|
||||
'example' => '',
|
||||
])
|
||||
->addRule('stderr', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'The script stderr output string. Logs the last 4,000 characters of the execution stderr output',
|
||||
'description' => 'The script stderr output string. Logs the last 4,000 characters of the execution stderr output. This will return an empty string unless the response is returned using an API key or as part of a webhook payload.',
|
||||
'default' => '',
|
||||
'example' => '',
|
||||
])
|
||||
|
|
|
@ -397,6 +397,9 @@ class FunctionsCustomClientTest extends Scope
|
|||
$this->assertEquals($this->getUser()['$id'], $output['APPWRITE_FUNCTION_USER_ID']);
|
||||
$this->assertNotEmpty($output['APPWRITE_FUNCTION_JWT']);
|
||||
$this->assertEquals($projectId, $output['APPWRITE_FUNCTION_PROJECT_ID']);
|
||||
// Client should never see logs and errors
|
||||
$this->assertEmpty($execution['body']['stdout']);
|
||||
$this->assertEmpty($execution['body']['stderr']);
|
||||
|
||||
// Cleanup : Delete function
|
||||
$response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
|
||||
|
|
|
@ -866,6 +866,7 @@ class FunctionsCustomServerTest extends Scope
|
|||
$this->assertEquals('', $output['APPWRITE_FUNCTION_USER_ID']);
|
||||
$this->assertEmpty($output['APPWRITE_FUNCTION_JWT']);
|
||||
$this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']);
|
||||
$this->assertStringContainsString('Amazing Function Log', $executions['body']['stdout']);
|
||||
|
||||
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
|
@ -893,7 +894,7 @@ class FunctionsCustomServerTest extends Scope
|
|||
|
||||
public function testCreateCustomNodeExecution()
|
||||
{
|
||||
$name = 'node-17.0';
|
||||
$name = 'node-18.0';
|
||||
$folder = 'node';
|
||||
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
|
||||
$this->packageCode($folder);
|
||||
|
@ -964,7 +965,7 @@ class FunctionsCustomServerTest extends Scope
|
|||
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
|
||||
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
|
||||
$this->assertEquals('Node.js', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
|
||||
$this->assertEquals('17.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
|
||||
$this->assertEquals('18.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
|
||||
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
|
||||
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT_DATA']);
|
||||
$this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
return function ($request, $response) {
|
||||
\var_dump("Amazing Function Log"); // We test logs (stdout) visibility with this
|
||||
|
||||
$response->json([
|
||||
'APPWRITE_FUNCTION_ID' => $request['env']['APPWRITE_FUNCTION_ID'],
|
||||
'APPWRITE_FUNCTION_NAME' => $request['env']['APPWRITE_FUNCTION_NAME'],
|
||||
|
|
Loading…
Reference in a new issue