Posted August 10, 2025
⚠️ To make use of google observability, you must enable billing against your google account. As of August 2025, it is not predicted that the level of use for a basic personal project of this nature will incur costs. The free tier allows up to 50GB and includes up to 30 days of log data retention - more details about google cloud billing can be found at google log pricing
Add the following roles to your service account:
⚠️ Security Note: Never commit this JSON file to version control. Store it securely in your password/key manager.
From your downloaded JSON file, extract:
private_key
client_email
project_id
client_id
⚠️ Make sure you copy the entire key including —–BEGIN PRIVATE KEY—– header, and —–END PRIVATE KEY—– footer
In your Apps Script editor:
// Set these as Script Properties in the Apps Script editor
PRIVATE_KEY: "-----BEGIN PRIVATE KEY-----\nYourActualPrivateKeyHere\n-----END PRIVATE KEY-----",
SERVICE_ACCOUNT_EMAIL: "scripting-logger@your-project-id.iam.gserviceaccount.com",
PROJECT_ID: "your-project-id",
CLIENT_ID: "your-client-id"
Alternative method using code:
Create a function to set the properties and run it
function setupProperties() {
const properties = PropertiesService.getScriptProperties();
// Replace with your actual values from the JSON file
properties.setProperties({
'PRIVATE_KEY': '-----BEGIN PRIVATE KEY-----\nYour-Private-Key-Here\n-----END PRIVATE KEY-----',
'SERVICE_ACCOUNT_EMAIL': 'scripting-logger@your-project-id.iam.gserviceaccount.com',
'PROJECT_ID': 'your-project-id',
'CLIENT_ID': 'your-client-id'
});
Logger.log('Properties set successfully');
}
Once you have run the function - remove it.
⚠️ Security Note: If you are extending this application beyond personal use, adjust to Google Secrets Manager for safer key management within the app
In Apps Script editor, click Libraries (+ icon in left sidebar)
In Google Apps Script, edit the script that was created in the previous tutorial.
// Configuration constants
const SERVICE_ACCOUNT_EMAIL = PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_EMAIL');
const PROJECT_ID = PropertiesService.getScriptProperties().getProperty('PROJECT_ID');
const CLIENT_ID = PropertiesService.getScriptProperties().getProperty('CLIENT_ID');
const PRIVATE_KEY = PropertiesService.getScriptProperties().getProperty('PRIVATE_KEY');
const DEBUG_LOGGING = false; // Set to true when debugging
// Validate required properties
if (!SERVICE_ACCOUNT_EMAIL || !PROJECT_ID || !PRIVATE_KEY) {
throw new Error('Missing required service account properties. Please check your Script Properties.');
}
/**
* Initialize OAuth2 Service for Google Cloud Logging
* @return {OAuth2Service} Configured OAuth2 service
*/
function getOAuthService() {
const service = OAuth2.createService('CloudLogging')
.setTokenUrl('https://oauth2.googleapis.com/token')
.setPrivateKey(PRIVATE_KEY)
.setClientId(CLIENT_ID)
.setIssuer(SERVICE_ACCOUNT_EMAIL)
.setPropertyStore(PropertiesService.getScriptProperties())
.setScope('https://www.googleapis.com/auth/logging.write');
return service;
}
/**
* Reset OAuth2 service (useful for debugging)
*/
function resetOAuthService() {
const service = getOAuthService();
service.reset();
Logger.log('OAuth service reset');
}
/**
* Send logs to Google Cloud Logging
* @param {string} message - Log message
* @param {string} severity - Log severity (DEFAULT, DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL)
* @param {string} logName - Custom log name (optional)
* @param {Object} jsonPayload - Structured data (optional)
*/
function sendLog(message, severity = 'INFO', logName = 'apps-script-log', jsonPayload = null) {
// Validate inputs
if (!message) {
throw new Error('Log message is required');
}
if (!PRIVATE_KEY) {
throw new Error('PRIVATE_KEY not found in Script Properties');
}
// Get OAuth2 service
const service = getOAuthService();
if (!service.hasAccess()) {
throw new Error('OAuth service authentication failed. Check service account configuration.');
}
// Construct log entry
const logEntry = {
logName: `projects/${PROJECT_ID}/logs/${logName}`,
resource: {
type: 'global'
},
entries: [{
severity: severity,
timestamp: new Date().toISOString(),
...(jsonPayload ? { jsonPayload } : { textPayload: message })
}]
};
// API request options
const options = {
method: 'POST',
contentType: 'application/json',
headers: {
'Authorization': `Bearer ${service.getAccessToken()}`
},
payload: JSON.stringify(logEntry)
};
// Send request
try {
const response = UrlFetchApp.fetch('https://logging.googleapis.com/v2/entries:write', options);
return {
success: true,
response: response.getContentText()
};
} catch (error) {
const errorMessage = `Failed to send log: ${error.toString()}`;
Logger.log(errorMessage);
throw new Error(errorMessage);
}
}
/**
* Log info message
*/
function logInfo(message, data = null) {
return sendLog(message, 'INFO', 'apps-script-info', data);
}
/**
* Log error message
*/
function logError(message, error = null) {
const errorData = error ? {
message: message,
error: error.toString(),
stack: error.stack
} : null;
return sendLog(message, 'ERROR', 'apps-script-errors', errorData);
}
/**
* Log warning message
*/
function logWarning(message, data = null) {
return sendLog(message, 'WARNING', 'apps-script-warnings', data);
}
/**
* Log debug message (only when DEBUG_LOGGING is true)
*/
function logDebug(message, data = null) {
if (DEBUG_LOGGING) {
return sendLog(message, 'DEBUG', 'apps-script-debug', data);
}
}
The easiest usage will include a few simple log lines in the doPost
method (created during previous tutorial).
This will be sufficient logging for a personal project.
⚠️ Best Practice Note: If you were to extend this project beyond personal use, consider updating to structured logging (for example: introduce correlation id’s, include payload headers, etc).
function doPost(e) {
try {
logInfo('Function started', { functionName: 'doPost', timestamp: new Date() });
const data = JSON.parse(e.postData.contents);
validateData(data);
// Process and store data
const result = logTimeEntry(data);
if (result) {
logInfo('Updated Spreadsheet Successfully', { result: result });
// Return with CORS headers for downstream apps
if (result) {
return ContentService
.createTextOutput(JSON.stringify({success: true, id: result}))
.setMimeType(ContentService.MimeType.JSON)
.setHeaders({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'Content-Type'
});
}
logError('Function doPost failed');
logDebug('debug data:', data); // Set DEBUG to true and run with that flag on, if you need to inspect the data/payload
throw new Error("No result from logTimeEntry");
} catch (error) {
logError('Function doPost failed', error);
logDebug('debug data:', data); // Set DEBUG to true and run with that flag on, if you need to inspect the data/payload
// Return with CORS headers for downstream apps
return ContentService
.createTextOutput(JSON.stringify({error: "Processing failed"}))
.setMimeType(ContentService.MimeType.JSON)
.setHeaders({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'Content-Type'
});
}
}
resource.type="global"
logName="projects/YOUR-PROJECT-ID/logs/apps-script-info"
When adding observability logs, you should always try to keep in-line with best practices: