Merge branch 'MDL-87045-main-minimal' of https://github.com/andrewnicols/moodle

This commit is contained in:
Adrian Greeve
2026-03-06 09:12:40 +07:00
committed by Huong Nguyen
15 changed files with 209 additions and 25 deletions
+2
View File
@@ -2211,6 +2211,8 @@ $string['targetrole'] = 'Target role';
$string['teacheronly'] = 'for the {$a} only';
$string['teacherroles'] = '{$a} roles';
$string['teachers'] = 'Teachers';
$string['telemetrytraceidfooterlink'] = 'Telemetry trace';
$string['telemetrytraceidfooterlinkcopied'] = 'The telemetry trace ID has been copied to your clipboard. You can now paste it in the support form or anywhere else you need.';
$string['textcopiedtoclipboard'] = 'Text copied to clipboard';
$string['textediting'] = 'Text editor';
$string['textediting_help'] = 'If an HTML editor such as Atto or TinyMCE is selected, text input areas will have a toolbar with buttons for easily adding content.
+1 -1
View File
@@ -8,6 +8,6 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 2.9
*/
define("core/ajax",["jquery","core/config","core/log","core/url"],(function($,config,Log,URL){var unloading=!1,requestSuccess=function(responses){var request,response,nosessionupdate,exception=null,i=0;if(responses.error)for(;i<this.length;i++)(request=this[i]).deferred.reject(responses);else{for(i=0;i<this.length;i++){if(request=this[i],void 0===(response=responses[i])){exception=new Error("missing response");break}if(!1!==response.error){exception=response.exception,nosessionupdate=this[i].nosessionupdate;break}request.deferred.resolve(response.data)}null!==exception&&("servicerequireslogin"!==exception.errorcode||nosessionupdate?this.forEach((function(request){request.deferred.reject(exception)})):window.location=URL.relativeUrl("/login/index.php"))}},requestFail=function(jqXHR,textStatus,exception){var i=0;for(i=0;i<this.length;i++){var request=this[i];unloading?(Log.error("Page unloaded."),Log.error(exception)):request.deferred.reject(exception)}};return{call:function(requests,async,loginrequired,nosessionupdate,timeout,cachekey){$(window).bind("beforeunload",(function(){unloading=!0}));var i,ajaxRequestData=[],promises=[],methodInfo=[],requestInfo="";for(void 0===loginrequired&&(loginrequired=!0),void 0===async&&(async=!0),void 0===timeout&&(timeout=0),void 0===cachekey||(cachekey=parseInt(cachekey))<=0?cachekey=null:cachekey||(cachekey=null),void 0===nosessionupdate&&(nosessionupdate=!1),i=0;i<requests.length;i++){var request=requests[i];ajaxRequestData.push({index:i,methodname:request.methodname,args:request.args}),request.nosessionupdate=nosessionupdate,request.deferred=$.Deferred(),promises.push(request.deferred.promise()),void 0!==request.done&&request.deferred.done(request.done),void 0!==request.fail&&request.deferred.fail(request.fail),request.index=i,methodInfo.push(request.methodname)}requestInfo=methodInfo.length<=5?methodInfo.sort().join():methodInfo.length+"-method-calls",ajaxRequestData=JSON.stringify(ajaxRequestData);var settings={type:"POST",context:requests,dataType:"json",processData:!1,async:async,contentType:"application/json",timeout:timeout},script="service.php",url=config.wwwroot+"/lib/ajax/";if(loginrequired?url+=script+"?sesskey="+config.sesskey+"&info="+requestInfo:(url+=(script="service-nologin.php")+"?info="+requestInfo,cachekey&&(url+="&cachekey="+cachekey,settings.type="GET")),nosessionupdate&&(url+="&nosessionupdate=true"),"POST"===settings.type)settings.data=ajaxRequestData;else{var urlUseGet=url+"&args="+encodeURIComponent(ajaxRequestData);urlUseGet.length>2e3?(settings.type="POST",settings.data=ajaxRequestData):url=urlUseGet}return async?$.ajax(url,settings).done(requestSuccess).fail(requestFail):(settings.success=requestSuccess,settings.error=requestFail,$.ajax(url,settings)),promises}}}));
define("core/ajax",["jquery","core/config","core/log","core/url"],(function($,config,Log,URL){var unloading=!1,requestSuccess=function(responses){var request,response,nosessionupdate,exception=null,i=0;if(responses.error)for(;i<this.length;i++)(request=this[i]).deferred.reject(responses);else{for(i=0;i<this.length;i++){if(request=this[i],void 0===(response=responses[i])){exception=new Error("missing response");break}if(!1!==response.error){exception=response.exception,nosessionupdate=this[i].nosessionupdate;break}request.deferred.resolve(response.data)}null!==exception&&("servicerequireslogin"!==exception.errorcode||nosessionupdate?this.forEach((function(request){request.deferred.reject(exception)})):window.location=URL.relativeUrl("/login/index.php"))}},requestFail=function(jqXHR,textStatus,exception){var i=0;for(i=0;i<this.length;i++){var request=this[i];unloading?(Log.error("Page unloaded."),Log.error(exception)):request.deferred.reject(exception)}};return{call:function(requests,async,loginrequired,nosessionupdate,timeout,cachekey){$(window).bind("beforeunload",(function(){unloading=!0}));var i,ajaxRequestData=[],promises=[],methodInfo=[],requestInfo="";for(void 0===loginrequired&&(loginrequired=!0),void 0===async&&(async=!0),void 0===timeout&&(timeout=0),void 0===cachekey||(cachekey=parseInt(cachekey))<=0?cachekey=null:cachekey||(cachekey=null),void 0===nosessionupdate&&(nosessionupdate=!1),i=0;i<requests.length;i++){var request=requests[i];ajaxRequestData.push({index:i,methodname:request.methodname,args:request.args}),request.nosessionupdate=nosessionupdate,request.deferred=$.Deferred(),promises.push(request.deferred.promise()),void 0!==request.done&&request.deferred.done(request.done),void 0!==request.fail&&request.deferred.fail(request.fail),request.index=i,methodInfo.push(request.methodname)}requestInfo=methodInfo.length<=5?methodInfo.sort().join():methodInfo.length+"-method-calls",ajaxRequestData=JSON.stringify(ajaxRequestData);var settings={type:"POST",context:requests,dataType:"json",processData:!1,async:async,contentType:"application/json",timeout:timeout,headers:{pageparent:config.traceId||""}},script="service.php",url=config.wwwroot+"/lib/ajax/";if(loginrequired?url+=script+"?sesskey="+config.sesskey+"&info="+requestInfo:(url+=(script="service-nologin.php")+"?info="+requestInfo,cachekey&&(url+="&cachekey="+cachekey,settings.type="GET")),nosessionupdate&&(url+="&nosessionupdate=true"),"POST"===settings.type)settings.data=ajaxRequestData;else{var urlUseGet=url+"&args="+encodeURIComponent(ajaxRequestData);urlUseGet.length>2e3?(settings.type="POST",settings.data=ajaxRequestData):url=urlUseGet}return async?$.ajax(url,settings).done(requestSuccess).fail(requestFail):(settings.success=requestSuccess,settings.error=requestFail,$.ajax(url,settings)),promises}}}));
//# sourceMappingURL=ajax.min.js.map
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -261,7 +261,10 @@ define(['jquery', 'core/config', 'core/log', 'core/url'], function($, config, Lo
processData: false,
async: async,
contentType: "application/json",
timeout: timeout
timeout: timeout,
headers: {
pageparent: config.traceId || '',
},
};
var script = 'service.php';
+3
View File
@@ -323,6 +323,9 @@ export default class Fetch {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
// Note: Do not use traceparent as this is used by the telemetry system to link requests from the
// same request together.
'pageparent': Cfg.traceId || '',
},
};
+1 -1
View File
@@ -574,7 +574,7 @@ class component {
// Always keep moodle_exception in place.
$keyclasses = [
\core\exception\moodle_exception::class,
\core\router\middleware\api_validation_middleware::class,
\core\telemetry::class,
];
foreach ($keyclasses as $classname) {
if (!array_key_exists($classname, $cache['classmap'])) {
@@ -4001,6 +4001,21 @@ EOD;
return !empty($this->communication_link());
}
/**
* Returns the telemetry trace id for the current page, if available.
*
* This can be used to correlate logs and telemetry data with specific page views.
* It is typically presented in the footer or somewhere inconspicious so that user's experiencing difficulties
* may be asked for it.
*
* The value is also passed in the Response header if required.
*
* @return string|null
*/
public function telemetry_traceid(): ?string {
return \core\telemetry::get_page_id();
}
/**
* Returns the communication link, complete with html.
*
@@ -338,6 +338,7 @@ class page_requirements_manager {
'siteId' => (int) SITEID,
'userId' => (int) $USER->id,
'deprecationignorelist' => !empty($CFG->jsdeprecationignorelist) ? $CFG->jsdeprecationignorelist : [],
'traceId' => \core\telemetry::get_trace_parent_id(),
];
if ($CFG->debugdeveloper) {
$this->M_cfg['developerdebug'] = true;
+49 -11
View File
@@ -43,7 +43,7 @@ class shutdown_manager {
*/
public static function initialize(): void {
if (self::$registered) {
debugging('Shutdown manager is already initialised!');
self::log('Shutdown manager is already initialised!');
return;
}
self::$registered = true;
@@ -76,6 +76,15 @@ class shutdown_manager {
}
}
/**
* Whether the shutdown manager initialized.
*
* @return bool
*/
public static function is_initialized(): bool {
return self::$registered;
}
/**
* Signal handler for SIGINT, and SIGTERM.
*
@@ -169,12 +178,17 @@ class shutdown_manager {
public static function shutdown_handler(): void {
global $DB;
// In case we caught an out of memory shutdown we increase memory limit to unlimited, so we can gracefully shut down.
raise_memory_limit(MEMORY_UNLIMITED);
if (function_exists('raise_memory_limit')) {
// In case we caught an out of memory shutdown we increase memory limit to unlimited,
// so we can gracefully shut down.
raise_memory_limit(MEMORY_UNLIMITED);
}
// Always ensure we know who the user is in access logs even if they
// were logged in a weird way midway through the request.
set_access_log_user();
if (function_exists('set_access_log_user')) {
// Always ensure we know who the user is in access logs even if they
// were logged in a weird way midway through the request.
set_access_log_user();
}
// Custom stuff first.
foreach (self::$callbacks as $data) {
@@ -189,7 +203,7 @@ class shutdown_manager {
// Handle DB transactions, session need to be written afterwards
// in order to maintain consistency in all session handlers.
if ($DB->is_transaction_started()) {
if ($DB && $DB->is_transaction_started()) {
if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) {
// This should not happen, it usually indicates wrong catching of exceptions,
// because all transactions should be finished manually or in default exception handler.
@@ -242,7 +256,12 @@ class shutdown_manager {
// phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative
error_log('Mem usage over ' . $apachereleasemem . ': marking Apache child for reaping.');
}
if (MDL_PERFTOLOG) {
$logperformance = MDL_PERFTOLOG;
$logperformance = $logperformance || !empty($PERF->perfdebugdeferred);
$logperformance = $logperformance && function_exists('get_performance_info');
if ($logperformance) {
$perf = get_performance_info();
// phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative
error_log("PERF: " . $perf['txt']);
@@ -251,6 +270,7 @@ class shutdown_manager {
$perf = get_performance_info();
echo $OUTPUT->select_element_for_replace('#perfdebugfooter', $perf['html']);
}
if (MDL_PERFINC) {
$inc = get_included_files();
$ts = 0;
@@ -274,9 +294,11 @@ class shutdown_manager {
}
}
// Close the current streaming element if any.
if ($OUTPUT->has_started()) {
echo $OUTPUT->close_element_for_append();
if ($OUTPUT) {
// Close the current streaming element if any.
if ($OUTPUT->has_started()) {
echo $OUTPUT->close_element_for_append();
}
}
// Print any closing buffered tags.
@@ -284,6 +306,22 @@ class shutdown_manager {
echo $CFG->closingtags;
}
}
/**
* Logging for the shutdown manager.
*
* @param string $value
*/
protected static function log(string $value): void {
if (function_exists('debugging')) {
// Use Moodle's debugging function if available.
debugging($value, DEBUG_DEVELOPER);
} else {
// Fallback to error_log if debugging is not available.
// This is useful for older PHP versions or when debugging is not set up.
error_log($value); // phpcs:ignore moodle.PHP.ForbiddenFunctions.FoundWithAlternative
}
}
}
// Alias this class to the old name.
+104
View File
@@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core;
use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Trace\LocalRootSpan;
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
use OpenTelemetry\API\Trace\Span;
use OpenTelemetry\Context\Context;
/**
* OpenTelemetry Telemetry manager class for Moodle.
*
* This class acts as a central point for managing telemetry in Moodle,
* providing methods to initialize the telemetry system, get the current page ID, and record exceptions.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class telemetry {
/**
* Get the page ID for the current request.
*
* If OpenTelemetry is not configured, or there is no current root span, this will return null.
*
* @return string|null
*/
public static function get_page_id(): ?string {
if (!static::is_available()) {
return null;
}
// Fetch the current root span, and return the trace ID as the page ID.
$rootspan = LocalRootSpan::current();
$rootcontext = $rootspan->getContext();
if ($rootcontext->isValid() === false) {
return null;
}
return $rootcontext->getTraceId();
}
/**
* A helper method to get the trace id for the current request.
*
* @return string|null Null if OpenTelemetry is not configured
*/
public static function get_trace_parent_id(): ?string {
if (!static::is_available()) {
return null;
}
$headers = [];
Globals::propagator()->inject($headers);
return $headers[TraceContextPropagator::TRACEPARENT] ?? null;
}
/**
* Whether Telemetry is available and configured.
*
* @return bool
*/
public static function is_available(): bool {
if (!class_exists(Globals::class)) {
return false;
}
if (extension_loaded('opentelemetry') === false) {
return false;
}
return true;
}
/**
* Record an error, or exception to the current span.
*
* @param \Throwable $ex The exception to record.
*/
public static function record_throwable(\Throwable $ex): void {
if (!static::is_available()) {
return;
}
$span = Span::fromContext(Context::getCurrent());
$span->recordException($ex);
}
}
+7 -5
View File
@@ -592,7 +592,12 @@ ini_set('include_path', $CFG->libdir . '/pear' . PATH_SEPARATOR . ini_get('inclu
// Register our classloader.
\core\component::register_autoloader();
// Early profiling start, based exclusively on config.php $CFG settings
// Register our shutdown manager, do NOT use register_shutdown_function().
if (\core\shutdown_manager::is_initialized() === false) {
\core\shutdown_manager::initialize();
}
// Early profiling start, based exclusively on config.php $CFG settings.
if (!empty($CFG->earlyprofilingenabled) && !defined('ABORT_AFTER_CONFIG_CANCEL')) {
require_once($CFG->libdir . '/xhprof/xhprof_moodle.php');
profiling_start();
@@ -778,10 +783,7 @@ if (!isset($CFG->debugdisplay)) {
ini_set('display_errors', '1');
}
// Register our shutdown manager, do NOT use register_shutdown_function().
\core\shutdown_manager::initialize();
// Verify upgrade is not running unless we are in a script that needs to execute in any case
// Verify upgrade is not running unless we are in a script that needs to execute in any case.
if (!defined('NO_UPGRADE_CHECK') and isset($CFG->upgraderunning)) {
if ($CFG->upgraderunning < time()) {
unset_config('upgraderunning');
+4 -1
View File
@@ -136,7 +136,10 @@ function get_whoops(): ?\Whoops\Run {
function default_exception_handler(Throwable $ex): void {
global $CFG, $DB, $OUTPUT, $USER, $FULLME, $SESSION, $PAGE;
// detect active db transactions, rollback and log as error
// Record the throwable in OpenTelemetry if it's available.
\core\telemetry::record_throwable($ex);
// Detect active db transactions, rollback and log as error.
abort_all_db_transactions();
if (($ex instanceof required_capability_exception) && !CLI_SCRIPT && !AJAX_SCRIPT && !empty($CFG->autologinguests) && !empty($USER->autologinguest)) {
+15 -2
View File
@@ -73,7 +73,17 @@
</div>
<div class="tool_usertours-resettourcontainer">
</div>
{{# output.telemetry_traceid }}
<div>
<span id="telemetry_traceid" class="d-none">
{{{ output.telemetry_traceid }}}
</span>
<a href="#" data-action="copytoclipboard" data-clipboard-target="#telemetry_traceid"
data-clipboard-success-message="{{#str}}telemetrytraceidfooterlinkcopied, core{{/str}}">
{{#str}}telemetrytraceidfooterlink, core{{/str}}
</a>
</div>
{{/ output.telemetry_traceid }}
{{{ output.standard_footer_html }}}
{{{ output.standard_end_of_body_html }}}
</div>
@@ -94,7 +104,10 @@
</div>
</footer>
{{#js}}
require(['theme_boost/footer-popover'], function(FooterPopover) {
require([
'theme_boost/footer-popover',
'core/copy_to_clipboard',
], function(FooterPopover) {
FooterPopover.init();
});
{{/js}}