This document outlines the caching strategies, performance optimization techniques, rate limiting approaches, and performance targets for the iTop Integration app. The goal is to minimize API load on iTop while providing responsive user experience in Nextcloud.
Technology: HTTP Cache-Control headers + Browser cache API
What to Cache:
- SVG icons (ticket.svg, PC.svg, etc.)
- Vue components (after build)
- Static assets
TTL: 24 hours for static assets
Implementation:
// In controller responses for icons
$response = new FileDisplayResponse($iconPath);
$response->cacheFor(86400); // 24 hours
$response->addHeader('Cache-Control', 'public, max-age=86400');
return $response;Benefits:
- Instant icon loading
- Reduced server requests
- Better offline experience
Technology: Nextcloud ICacheFactory (Redis/Memcached/APCu)
What to Cache:
| Data Type | Cache Layer | Cache Key Pattern | Default TTL | Config Key | Invalidation |
|---|---|---|---|---|---|
| API Queries (CIs, tickets, searches) | ItopAPIService | api:{md5(key)} |
60s | cache_ttl_api_query |
TTL only |
| CI Previews | ItopReferenceProvider (via CacheService) | ci_preview:{userId}:{class}:{id} |
60s | (same as API) | Manual or TTL |
| Search Results | ItopSearchProvider | api:{md5(query)} |
30s | (planned) | TTL only |
| Profile Data | ProfileService | profile:{userId} |
300s | (planned) | Manual or TTL |
| User Info | ItopAPIService | api:{md5(key)} |
600s | (planned) | Manual |
| Picker Results | ItopReferenceProvider.search() | api:{md5(query)} |
60s | (planned) | TTL only |
| Ticket Lists | ItopAPIService | api:{md5(key)} |
120s | (planned) | TTL only |
Key: (planned) = future admin configuration in Phase 6
Implementation:
class CacheService {
public function __construct(
private ICacheFactory $cacheFactory,
private LoggerInterface $logger
) {
$this->cache = $cacheFactory->createDistributed('integration_itop');
}
public function get(string $key): mixed {
try {
$value = $this->cache->get($key);
if ($value !== null) {
$this->logger->debug('Cache HIT', ['key' => $key]);
return json_decode($value, true);
}
$this->logger->debug('Cache MISS', ['key' => $key]);
return null;
} catch (\Exception $e) {
$this->logger->warning('Cache get failed', ['key' => $key, 'error' => $e->getMessage()]);
return null;
}
}
public function set(string $key, mixed $value, int $ttl): void {
try {
$this->cache->set($key, json_encode($value), $ttl);
$this->logger->debug('Cache SET', ['key' => $key, 'ttl' => $ttl]);
} catch (\Exception $e) {
$this->logger->warning('Cache set failed', ['key' => $key, 'error' => $e->getMessage()]);
}
}
public function delete(string $key): void {
$this->cache->remove($key);
$this->logger->debug('Cache DELETE', ['key' => $key]);
}
public function clear(string $prefix): void {
$this->cache->clear($prefix);
$this->logger->info('Cache CLEAR', ['prefix' => $prefix]);
}
}Use Case: Nextcloud instances with multiple app servers
Technology: Redis (recommended) or Memcached
Configuration:
// config/config.php
'memcache.distributed' => '\OC\Memcache\Redis',
'redis' => [
'host' => 'localhost',
'port' => 6379,
'timeout' => 1.5,
],Key Namespacing:
integration_itop/user_{userId}/search_{hash}
integration_itop/user_{userId}/preview_{class}_{id}
Benefits:
- Shared cache across multiple servers
- High availability
- Fast access (~1ms)
Status: β Implemented (Phase 2-3)
Overview: The ItopAPIService->request() method implements automatic query-based caching independent of user ID. All API responses are cached based on the OQL query or ID provided, allowing multiple users to share the same cache entry for identical queries.
1. User A requests CI id=32
ββ Build cache key: md5("32") β "c4ca4238a0b923820dcc509a6f75849b"
ββ Check cache: cache.get("api:c4ca4238...") β null (MISS)
ββ Call iTop API with app token
ββ Add metadata: _cache_timestamp + _cache_ttl
ββ Store in cache: cache.set(..., 60s TTL)
2. User B requests same CI id=32 (within 60s)
ββ Build same cache key: md5("32")
ββ Check cache: cache.get("api:c4ca4238...") β hit!
ββ Validate: age (15s) < ttl (60s) β
ββ Return cached result (no API call)
3. User A requests CI id=32 after 60s
ββ Build cache key: md5("32")
ββ Check cache: cache.get(...) β null (expired)
ββ Call iTop API again
ββ Cache refreshed with new TTL
Cache Key Generation:
$key = $params['key'] ?? ''; // OQL query or ID
$keyHash = md5($key); // Hash for reasonable length
return 'api:' . $keyHash; // Final key: api:c4ca4238...Examples of Query Keys:
Direct ID lookup:
$params['key'] = "32"
Cache key: api:c4ca4238a0b923820dcc509a6f75849b
OQL query:
$params['key'] = "SELECT PC WHERE name LIKE '%APC0001%'"
Cache key: api:a1b2c3d4e5f6...
Search query:
$params['key'] = "SELECT Software AS ci WHERE ci.name LIKE '%Office%'"
Cache key: api:f6e5d4c3b2a1...
Metadata Storage: Cache payload includes internal metadata for explicit TTL validation:
{
"code": 0,
"message": "Found: 1",
"objects": {...},
"_cache_timestamp": 1729886322,
"_cache_ttl": 60
}Metadata is stripped before returning to callers.
Multi-User Cache Sharing:
- User A requests PC id=32 β cache miss β API call β cache stored
- User B requests PC id=32 β cache hit β serves same entry
- Same query = same cache key = shared cache entry
- No userId in cache key (intentional for efficiency)
Admin Settings (Phase 6): Administrators can configure TTL per data type via appconfig:
| Config Key | Default | Description | Planned |
|---|---|---|---|
cache_ttl_api_query |
60s | General API query caching (CIs, searches, tickets) | β Implemented |
cache_ttl_search |
30s | Search results specifically | Phase 6 |
cache_ttl_profile |
300s | User profile/permission data | Phase 6 |
cache_ttl_ci_preview |
60s | CI preview rendering | Phase 6 |
cache_ttl_tickets |
120s | Ticket list data | Phase 6 |
Code Pattern:
// Get TTL from admin config with default fallback
$defaultTTL = 60;
$ttl = (int)$this->config->getAppValue('integration_itop', 'cache_ttl_api_query', $defaultTTL);
// Cache if TTL > 0 (allows disabling cache with TTL=0)
if ($ttl > 0) {
$this->cache->set($cacheKey, json_encode($data), $ttl);
}Cache operations are logged with debug level for monitoring:
API query cache HIT: age=15s, ttl=60s, class=PC
API query cache MISS: cacheKey=api:c4ca4238...
API query CACHED: cacheKey=api:c4ca4238..., ttl=60s
CI Previews: 60 seconds
private const CACHE_TTL_CI_PREVIEW = 60;Rationale:
- CIs can be updated frequently
- Balance between freshness and performance
- Acceptable staleness for most use cases
Search Results: 30 seconds
private const CACHE_TTL_SEARCH = 30;Rationale:
- Users expect recent results in search
- Search queries vary widely (low cache hit rate)
- Short TTL reduces stale results
Picker Suggestions: 60 seconds
private const CACHE_TTL_PICKER = 60;Rationale:
- Similar to search but slightly longer
- Users may repeat similar queries while typing
Ticket Lists: 120 seconds
private const CACHE_TTL_TICKET_LIST = 120;Rationale:
- Dashboard widgets tolerate slight staleness
- Reduce API load for frequently viewed dashboards
Profile Data: 300 seconds (5 minutes)
private const CACHE_TTL_PROFILE = 300;Rationale:
- User profiles rarely change
- Portal/power user status is relatively stable
- 5 minutes balances freshness with performance
User Info: 600 seconds (10 minutes)
private const CACHE_TTL_USER_INFO = 600;Rationale:
- Person name, email, org rarely change
- Used for display only (not auth)
Application Config: No expiry (manual invalidation)
// Stored in appconfig table, not cache
$url = $this->config->getAppValue('integration_itop', 'admin_instance_url');Status: β Implemented (Phase 6)
Administrators can now configure cache TTL values via the Nextcloud admin settings panel without modifying code. This allows fine-tuning performance based on deployment requirements.
- Navigate to Settings β Administration β iTop Integration
- Scroll to the Cache & Performance Settings section
- Configure TTL values for each cache type
- Click Save Cache Settings to apply changes
| Parameter | Default | Range | Description |
|---|---|---|---|
| CI Preview Cache TTL | 60s | 10β3600s (1h) | How long to cache Configuration Item preview data. Lower values = fresher data but higher API load; higher values = better performance. |
| Ticket Info Cache TTL | 60s | 10β3600s (1h) | How long to cache ticket preview data (UserRequest and Incident previews). |
| Search Results Cache TTL | 30s | 10β300s (5min) | How long to cache search results. Shorter TTLs ensure fresher results but increase API load. |
| Picker Suggestions Cache TTL | 60s | 10β300s (5min) | How long to cache Smart Picker suggestions for CI links in Text/Talk. |
The admin panel also provides a Clear All Cache button that immediately invalidates all cached entries. This is useful:
- After major iTop data changes
- When troubleshooting stale data issues
- During testing and development
Warning: Clearing cache will temporarily reduce performance until the cache is rebuilt through normal usage.
CI Preview TTL: 10s (frequent changes expected)
Ticket Info TTL: 10s (rapid testing cycles)
Search TTL: 10s (immediate feedback needed)
Picker TTL: 10s (testing different queries)
CI Preview TTL: 30s (balance freshness vs performance)
Ticket Info TTL: 30s (frequent ticket updates)
Search TTL: 20s (users expect recent results)
Picker TTL: 30s (similar to search)
CI Preview TTL: 300s (CIs change infrequently)
Ticket Info TTL: 120s (moderate ticket activity)
Search TTL: 60s (acceptable staleness)
Picker TTL: 120s (longer cache for performance)
CI Preview TTL: 600s (maximize cache hits)
Ticket Info TTL: 300s (reduce iTop API load)
Search TTL: 120s (balance load vs freshness)
Picker TTL: 180s (optimize for performance)
Backend Storage:
Cache TTL values are stored in Nextcloud's appconfig table:
cache_ttl_ci_preview β integer (10-3600 seconds)
cache_ttl_ticket_info β integer (10-3600 seconds)
cache_ttl_search β integer (10-300 seconds)
cache_ttl_picker β integer (10-300 seconds)Validation:
- CI Preview and Ticket Info TTL: 10s minimum, 3600s (1 hour) maximum
- Search and Picker TTL: 10s minimum, 300s (5 minutes) maximum
- Values are validated server-side before saving
Live Updates: TTL changes take effect immediately:
- New cache entries use the updated TTL
- Existing cache entries retain their original TTL until expiration
- Clear cache after changing TTLs if immediate effect is needed
The cache configuration feature exposes these endpoints:
Get Current Settings:
GET /apps/integration_itop/admin-config
Response: {
"cache_ttl_ci_preview": 60,
"cache_ttl_ticket_info": 60,
"cache_ttl_search": 30,
"cache_ttl_picker": 60,
...
}
Save Cache Settings:
POST /apps/integration_itop/cache-settings
Body: {
"ciPreviewTTL": 120,
"ticketInfoTTL": 120,
"searchTTL": 60,
"pickerTTL": 90
}
Response: {
"message": "Cache settings saved successfully",
"cache_ttl_ci_preview": 120,
...
}
Clear All Cache:
POST /apps/integration_itop/clear-cache
Response: {
"message": "All cache entries cleared successfully"
}
{app_id}/{user_scope}/{data_type}_{identifier}
Examples:
integration_itop/user_boris/search_5f4dcc3b5aa765d61d8327deb882cf99
integration_itop/user_boris/preview_PC_5
integration_itop/user_boris/profile
integration_itop/global/ci_classes
Critical: Always include user ID in cache keys for user-specific data
// WRONG - shared across users!
$cacheKey = 'search_' . md5($query);
// CORRECT - isolated per user
$cacheKey = 'search_' . $userId . '_' . md5($query);Rationale:
- Prevents data leakage between users
- Respects portal user permissions
- Essential for security
private function generateCacheKey(string $userId, string $type, array $params): string {
$hash = md5(json_encode($params));
return "integration_itop/user_{$userId}/{$type}_{$hash}";
}
// Example
$key = $this->generateCacheKey('boris', 'search', [
'query' => 'laptop',
'classes' => ['PC', 'Phone'],
'limit' => 20
]);
// Result: integration_itop/user_boris/search_a3f8b1c2d4e5f6...Default Strategy: Let cache expire naturally
Implementation: Automatic by cache backend
Pros:
- Simple, no code needed
- Predictable behavior
Cons:
- May serve stale data
- Cannot force refresh
Trigger Events:
- User updates personal settings
- Admin updates application token
- User explicitly refreshes
Implementation:
public function invalidateUserCache(string $userId): void {
$prefix = "integration_itop/user_{$userId}/";
$this->cacheService->clear($prefix);
$this->logger->info('Invalidated user cache', ['user' => $userId]);
}
// Called from settings controller
public function setConfig(array $values): DataResponse {
// Save config...
// Invalidate cache
$this->cacheService->invalidateUserCache($this->userId);
return new DataResponse(['success' => true]);
}Use Case: Invalidate only specific cache entries
public function invalidateCIPreview(string $userId, string $class, int $id): void {
$key = "integration_itop/user_{$userId}/preview_{$class}_{$id}";
$this->cacheService->delete($key);
}Strategy: Pre-populate cache for likely requests
public function warmDashboardCache(string $userId): void {
// Pre-fetch ticket counts
$this->getTicketCounts($userId);
// Pre-fetch recent tickets
$this->getRecentTickets($userId, 5);
$this->logger->info('Dashboard cache warmed', ['user' => $userId]);
}
// Call after user loginImplementation:
class RateLimiter {
private const MAX_REQUESTS_PER_SECOND = 5;
private const WINDOW_SECONDS = 1;
public function checkLimit(string $userId): bool {
$key = 'rate_limit_' . $userId;
$count = (int) $this->cache->get($key);
if ($count >= self::MAX_REQUESTS_PER_SECOND) {
return false; // Rate limit exceeded
}
$this->cache->set($key, $count + 1, self::WINDOW_SECONDS);
return true;
}
}
// In controller
if (!$this->rateLimiter->checkLimit($userId)) {
return new DataResponse(
['error' => 'Rate limit exceeded'],
Http::STATUS_TOO_MANY_REQUESTS
);
}More Accurate: Prevents burst at window boundaries
class SlidingWindowRateLimiter {
private const MAX_REQUESTS = 5;
private const WINDOW_MS = 1000;
public function checkLimit(string $userId): bool {
$key = 'rate_limit_sliding_' . $userId;
$now = microtime(true) * 1000; // milliseconds
// Get timestamps of recent requests
$timestamps = json_decode($this->cache->get($key) ?? '[]', true);
// Remove old timestamps (outside window)
$timestamps = array_filter($timestamps, fn($ts) => $ts > $now - self::WINDOW_MS);
if (count($timestamps) >= self::MAX_REQUESTS) {
return false;
}
// Add current timestamp
$timestamps[] = $now;
$this->cache->set($key, json_encode($timestamps), 2); // 2 seconds TTL
return true;
}
}| Feature | Limit | Window |
|---|---|---|
| Unified Search | 10 req/min | 60s |
| Smart Picker | 20 req/min | 60s |
| Rich Preview | 30 req/min | 60s |
| Dashboard Widget | 5 req/min | 60s |
Implementation:
private function getRateLimitForFeature(string $feature): array {
return match($feature) {
'search' => ['limit' => 10, 'window' => 60],
'picker' => ['limit' => 20, 'window' => 60],
'preview' => ['limit' => 30, 'window' => 60],
'dashboard' => ['limit' => 5, 'window' => 60],
default => ['limit' => 5, 'window' => 1]
};
}Overview: The application uses multiple caching layers across different services. Understanding how they interact is critical for debugging and optimization.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nextcloud App β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Layer 1: UI Components (Vue) β β
β β ββ ReferenceItopWidget (CI preview display) β β
β β ββ ItopSearchProvider results β β
β β ββ ItopSmartPickerProvider suggestions β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Layer 2: Services (PHP) β β
β β ββ ItopReferenceProvider β β
β β β ββ Uses CacheService for CI preview caching β β
β β ββ ItopSearchProvider β β
β β β ββ Calls ItopAPIService.searchCIs() β β
β β ββ ProfileService β β
β β ββ Determines user permissions (portal vs power) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Layer 3: API Service (ItopAPIService) β β
β β ββ buildQueryCacheKey() β md5(key) β β
β β ββ Cache check: cache.get(api:hash) β HIT/MISS β β
β β ββ If MISS: Call iTop REST API β β
β β ββ Add metadata: _cache_timestamp, _cache_ttl β β
β β ββ Cache store: cache.set(api:hash, $data, $ttl) β β
β β ββ Methods: β β
β β - getCIPreview() β Single CI by ID β β
β β - searchCIs() β OQL search query β β
β β - getTicketInfo() β Single ticket by ID β β
β β - search() β Broad search (tickets + CIs) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Layer 4: Cache Backend (Nextcloud ICacheFactory) β β
β β ββ Redis (recommended for multi-server) β β
β β ββ Memcached (alternative) β β
β β ββ APCu (single-server) β β
β β ββ File cache (fallback) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Layer 5: iTop API (External) β β
β β ββ REST endpoint: /webservices/rest.php β β
β β ββ Authentication: App token (admin-configured) β β
β β ββ Queries: SELECT OQL or core/get operations β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Request from UI Component
β
[Check Scope]
ββ CI Preview? β Use CacheService (user-isolated)
ββ Search/Pick? β Use ItopAPIService (query-based)
ββ Other? β Direct API call
β
[Build Cache Key]
ββ CacheService: ci_preview:userId:class:id
ββ ItopAPIService: api:md5(oql_query)
β
[Check Cache]
ββ HIT: Validate TTL & timestamp β Return
ββ MISS: Call iTop API
β
[Process Response]
ββ Success: Add metadata + cache
ββ Error: Return without caching
β
[Strip Metadata]
ββ Remove _cache_timestamp, _cache_ttl before returning
β
Response to UI
| Service | Cache Type | Key Pattern | TTL Config | Sharing |
|---|---|---|---|---|
| ItopAPIService | Query-based | api:{md5(key)} |
cache_ttl_api_query |
Cross-user |
| ItopReferenceProvider | CI preview | ci_preview:{userId}:{class}:{id} |
Uses API TTL | Per-user |
| ItopSearchProvider | Search results | Via ItopAPIService | cache_ttl_api_query |
Cross-user |
| ItopSmartPickerProvider | Picker results | Via ItopAPIService | cache_ttl_api_query |
Cross-user |
| ProfileService | Profile data | profile:{userId} |
cache_ttl_profile (Phase 6) |
Per-user |
Example 1: Getting CI Preview (PC id=32)
ReferenceItopWidget (Vue component)
ββ getCIReference() in ItopReferenceProvider
ββ CacheService.getCIPreview(userId, 'PC', 32)
β ββ Key: ci_preview:boris:PC:32
β ββ HIT: Return cached preview
β ββ MISS: Continue to API
ββ ItopAPIService.getCIPreview(userId, 'PC', 32)
ββ buildQueryCacheKey() β api:c4ca4238...
ββ cache.get() β MISS
ββ request(userId, {operation, class, key='32', output_fields='*'})
β ββ POST /webservices/rest.php?version=1.3
β ββ Auth-Token: [app_token]
ββ Store in cache with _cache_timestamp + _cache_ttl
Example 2: Searching Software (term="Office")
ItopSearchProvider.search("Office")
ββ ItopAPIService.searchCIs(userId, "Office", [], isPortalOnly)
ββ buildQueryCacheKey() β api:a1b2c3d4...
ββ cache.get() β MISS
ββ request(userId, {operation: 'core/get', class: 'Software', key: "SELECT Software ... LIKE '%Office%'", ...})
ββ Multiple API calls per class (if needed)
ββ Cache entire result set once
Example 3: Multiple Users, Same Query
User A: getCIPreview(PC, 32)
ββ buildQueryCacheKey() β api:c4ca4238... β MISS β API call β CACHED
User B: getCIPreview(PC, 32)
ββ buildQueryCacheKey() β api:c4ca4238... β HIT! β No API call
ββ Result served from cache (same entry as User A)
| Operation | Target | Cached | Uncached |
|---|---|---|---|
| Search | <500ms | <100ms | <2s |
| Preview | <300ms | <50ms | <1s |
| Picker | <400ms | <80ms | <1.5s |
| Dashboard | <600ms | <150ms | <2s |
| Settings Load | <200ms | N/A | <500ms |
class PerformanceMonitor {
public function measure(string $operation, callable $callback): mixed {
$start = microtime(true);
try {
$result = $callback();
$duration = (microtime(true) - $start) * 1000; // milliseconds
$this->logger->info('Performance', [
'operation' => $operation,
'duration_ms' => round($duration, 2),
'cached' => $this->wasCacheHit()
]);
return $result;
} catch (\Exception $e) {
$duration = (microtime(true) - $start) * 1000;
$this->logger->error('Performance (failed)', [
'operation' => $operation,
'duration_ms' => round($duration, 2),
'error' => $e->getMessage()
]);
throw $e;
}
}
}
// Usage
$results = $this->perfMonitor->measure('search', function() {
return $this->itopAPIService->search($userId, $query);
});| Data Type | Target Hit Rate |
|---|---|
| Previews | >80% |
| Search | >60% |
| Picker | >70% |
| Profiles | >90% |
Monitoring:
class CacheMetrics {
public function recordHit(string $type): void {
$key = 'cache_metrics_' . $type . '_hits';
$this->cache->inc($key);
}
public function recordMiss(string $type): void {
$key = 'cache_metrics_' . $type . '_misses';
$this->cache->inc($key);
}
public function getHitRate(string $type): float {
$hits = (int) $this->cache->get('cache_metrics_' . $type . '_hits');
$misses = (int) $this->cache->get('cache_metrics_' . $type . '_misses');
if ($hits + $misses === 0) {
return 0.0;
}
return $hits / ($hits + $misses);
}
}Defer non-critical API calls:
<template>
<div class="dashboard-widget">
<div v-if="!loaded" class="skeleton">Loading...</div>
<div v-else>
<!-- Widget content -->
</div>
</div>
</template>
<script>
export default {
data() {
return {
loaded: false,
tickets: []
}
},
async mounted() {
// Delay loading until component is visible
if (this.isVisible()) {
await this.loadTickets()
} else {
this.observeVisibility()
}
},
methods: {
observeVisibility() {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.loadTickets()
observer.disconnect()
}
})
observer.observe(this.$el)
}
}
}
</script>Search Input Debouncing:
import { debounce } from '@nextcloud/vue'
export default {
methods: {
onSearchInput: debounce(async function(query) {
const results = await this.search(query)
this.displayResults(results)
}, 300) // Wait 300ms after user stops typing
}
}Scroll Event Throttling:
import { throttle } from 'lodash'
export default {
mounted() {
window.addEventListener('scroll', throttle(this.onScroll, 100))
},
methods: {
onScroll() {
// Handle scroll (max once per 100ms)
}
}
}Batch Multiple CI Requests:
public function getMultipleCIPreviews(string $userId, array $items): array {
// items: [['class' => 'PC', 'id' => 5], ['class' => 'Phone', 'id' => 10], ...]
// Group by class
$grouped = [];
foreach ($items as $item) {
$grouped[$item['class']][] = $item['id'];
}
// Single query per class
$results = [];
foreach ($grouped as $class => $ids) {
$query = "SELECT $class WHERE id IN (" . implode(',', $ids) . ")";
$classResults = $this->itopAPIService->query($query);
$results = array_merge($results, $classResults);
}
return $results;
}Minimize Output Fields:
// List view - lightweight
$listFields = 'id,name,status,last_update';
// Preview view - comprehensive
$previewFields = 'id,name,status,org_id_friendlyname,brand_id_friendlyname,model_id_friendlyname,cpu,ram,description';
// Only request what's needed
$query = [
'operation' => 'core/get',
'class' => 'PC',
'key' => 'SELECT PC LIMIT 10',
'output_fields' => $context === 'list' ? $listFields : $previewFields
];Enable Gzip Compression:
// In API requests to iTop
$options = [
'headers' => [
'Accept-Encoding' => 'gzip, deflate'
]
];Enable Verbose Logging:
// In admin settings
$debugMode = $this->config->getAppValue('integration_itop', 'debug_mode', '0') === '1';
if ($debugMode) {
$this->logger->debug('Cache check', [
'key' => $cacheKey,
'hit' => $cached !== null,
'ttl_remaining' => $this->cache->ttl($cacheKey)
]);
}Admin Page Showing:
- Cache hit rates per type
- Average response times
- Rate limit violations
- Top slowest queries
- Architecture: architecture.md
- API Integration: itop-api.md
- Observability: observability.md