diff --git a/src/main/java/org/operaton/fitpub/service/WeatherService.java b/src/main/java/org/operaton/fitpub/service/WeatherService.java index cad292e..1ec918d 100644 --- a/src/main/java/org/operaton/fitpub/service/WeatherService.java +++ b/src/main/java/org/operaton/fitpub/service/WeatherService.java @@ -52,20 +52,24 @@ public class WeatherService { @Transactional public Optional fetchWeatherForActivity(Activity activity) { log.info("=== Weather fetch requested for activity {} ===", activity.getId()); - log.info("Weather enabled: {}, API key configured: {}", weatherEnabled, (apiKey != null && !apiKey.isBlank())); + log.info("Weather configuration: enabled={}, API key configured={}, API key length={}", + weatherEnabled, (apiKey != null && !apiKey.isBlank()), + (apiKey != null ? apiKey.length() : 0)); if (!weatherEnabled) { - log.warn("Weather fetching is DISABLED in configuration (fitpub.weather.enabled=false)"); + log.warn("Weather fetching is DISABLED in configuration (fitpub.weather.enabled=false). " + + "Set fitpub.weather.enabled=true in application properties to enable."); return Optional.empty(); } if (apiKey == null || apiKey.isBlank()) { - log.error("Weather API key is NOT CONFIGURED (fitpub.weather.api-key is empty)"); + log.error("Weather API key is NOT CONFIGURED (fitpub.weather.api-key is empty). " + + "Please set fitpub.weather.api-key in application properties."); return Optional.empty(); } - log.debug("Weather API key present (length: {} chars, starts with: {}...)", - apiKey.length(), apiKey.length() > 4 ? apiKey.substring(0, 4) : "???"); + log.info("Weather API key present: length={} chars, first 4 chars={}...", + apiKey.length(), apiKey.length() > 4 ? apiKey.substring(0, 4) : "???"); // Check if weather data already exists if (weatherDataRepository.existsByActivityId(activity.getId())) { @@ -93,19 +97,18 @@ public class WeatherService { } JsonNode firstPoint = trackPoints.get(0); - - log.debug("First track point missing lat/lon for activity {}", activity.getId()); + log.debug("First track point: {}", firstPoint.toString()); // Check if lat/lon fields exist if (!firstPoint.has("lat") || !firstPoint.has("lon")) { - log.error("First track point missing lat/lon fields for activity {}", - activity.getId()); + log.error("First track point MISSING lat/lon fields for activity {}. Available fields: {}", + activity.getId(), firstPoint.fieldNames()); return Optional.empty(); } double lat = firstPoint.get("lat").asDouble(); double lon = firstPoint.get("lon").asDouble(); - log.info("Extracted location: lat={}, lon={}", lat, lon); + log.info("Extracted location from first track point: lat={}, lon={}", lat, lon); // Check if activity is recent (within 5 days) - use current weather API // Otherwise use historical data API (requires paid plan) @@ -117,20 +120,25 @@ public class WeatherService { WeatherData weatherData; if (daysDifference <= 5) { - log.info("Activity is recent (within 5 days), fetching current weather"); + log.info("Activity is RECENT ({} days old, within 5 day threshold), fetching current weather from OpenWeatherMap", daysDifference); weatherData = fetchCurrentWeather(lat, lon, activity.getId()); } else { - log.warn("Activity is {} days old (>5 days), historical weather data requires paid API plan. Skipping.", daysDifference); + log.warn("Activity is {} days old (exceeds 5 day threshold). Historical weather data requires OpenWeatherMap paid API plan. Skipping weather fetch.", daysDifference); return Optional.empty(); } if (weatherData != null) { - log.info("Successfully fetched and parsed weather data, saving to database"); - WeatherData saved = weatherDataRepository.save(weatherData); - log.info("Weather data saved with ID: {}", saved.getId()); - return Optional.of(saved); + log.info("Successfully fetched and parsed weather data. Attempting to save to database..."); + try { + WeatherData saved = weatherDataRepository.save(weatherData); + log.info("Weather data SUCCESSFULLY SAVED to database with ID: {}", saved.getId()); + return Optional.of(saved); + } catch (Exception e) { + log.error("FAILED to save weather data to database: {}", e.getMessage(), e); + return Optional.empty(); + } } else { - log.error("Weather data fetch returned null"); + log.error("Weather data fetch returned NULL - check API errors above"); } } catch (Exception e) { @@ -145,54 +153,93 @@ public class WeatherService { * Fetch current weather data from OpenWeatherMap. */ private WeatherData fetchCurrentWeather(double lat, double lon, UUID activityId) { + log.info("=== fetchCurrentWeather START === activityId={}, lat={}, lon={}", activityId, lat, lon); try { String url = String.format("%s?lat=%f&lon=%f&appid=%s&units=metric", OPENWEATHERMAP_API_URL, lat, lon, apiKey); String maskedUrl = url.replace(apiKey, "***API_KEY***"); - log.info("Making API request to OpenWeatherMap: {}", maskedUrl); - log.debug("API URL: {}", OPENWEATHERMAP_API_URL); - log.debug("Coordinates: lat={}, lon={}", lat, lon); + log.info("Constructed OpenWeatherMap API URL: {}", maskedUrl); + log.info("Request parameters: lat={}, lon={}, units=metric", lat, lon); long startTime = System.currentTimeMillis(); + log.info("Sending HTTP GET request to OpenWeatherMap..."); String response = restTemplate.getForObject(URI.create(url), String.class); long duration = System.currentTimeMillis() - startTime; - log.info("API request completed in {}ms", duration); + log.info("HTTP request completed in {}ms, response received", duration); if (response == null) { - log.error("API response is NULL - no data returned from OpenWeatherMap"); + log.error("API response is NULL - RestTemplate returned null, no data from OpenWeatherMap"); return null; } - log.debug("API response length: {} chars", response.length()); - log.debug("API response preview: {}", response.length() > 200 ? response.substring(0, 200) + "..." : response); + log.info("API response received: {} characters", response.length()); + log.info("API response (first 300 chars): {}", + response.length() > 300 ? response.substring(0, 300) + "..." : response); + log.info("Parsing weather response JSON..."); WeatherData weatherData = parseWeatherResponse(response, activityId); if (weatherData == null) { - log.error("Failed to parse weather response"); + log.error("FAILED to parse weather response - see parsing errors above"); } else { - log.info("Successfully parsed weather data: temp={}°C, condition={}", - weatherData.getTemperatureCelsius(), weatherData.getWeatherCondition()); + log.info("Successfully parsed weather data: temp={}°C, feels_like={}°C, condition='{}', description='{}', humidity={}%, pressure={} hPa, wind={} m/s", + weatherData.getTemperatureCelsius(), + weatherData.getFeelsLikeCelsius(), + weatherData.getWeatherCondition(), + weatherData.getWeatherDescription(), + weatherData.getHumidity(), + weatherData.getPressure(), + weatherData.getWindSpeedMps()); } + log.info("=== fetchCurrentWeather END === success={}", (weatherData != null)); return weatherData; } catch (org.springframework.web.client.HttpClientErrorException e) { - log.error("HTTP CLIENT ERROR from OpenWeatherMap API: Status={}, Body={}", - e.getStatusCode(), e.getResponseBodyAsString(), e); + log.error("=== HTTP CLIENT ERROR (4xx) from OpenWeatherMap API ==="); + log.error("Status Code: {}", e.getStatusCode()); + log.error("Status Text: {}", e.getStatusText()); + log.error("Response Body: {}", e.getResponseBodyAsString()); + log.error("Request URL (masked): {}", OPENWEATHERMAP_API_URL + "?lat=" + lat + "&lon=" + lon + "&appid=***&units=metric"); + if (e.getStatusCode().value() == 401) { + log.error("AUTHENTICATION FAILED - Check your OpenWeatherMap API key is valid and active"); + } else if (e.getStatusCode().value() == 404) { + log.error("API ENDPOINT NOT FOUND - Check coordinates are valid: lat={}, lon={}", lat, lon); + } else if (e.getStatusCode().value() == 429) { + log.error("RATE LIMIT EXCEEDED - Too many API requests. Check your OpenWeatherMap plan limits."); + } + log.error("Exception details:", e); return null; } catch (org.springframework.web.client.HttpServerErrorException e) { - log.error("HTTP SERVER ERROR from OpenWeatherMap API: Status={}, Body={}", - e.getStatusCode(), e.getResponseBodyAsString(), e); + log.error("=== HTTP SERVER ERROR (5xx) from OpenWeatherMap API ==="); + log.error("Status Code: {}", e.getStatusCode()); + log.error("Status Text: {}", e.getStatusText()); + log.error("Response Body: {}", e.getResponseBodyAsString()); + log.error("OpenWeatherMap service may be experiencing issues. Try again later."); + log.error("Exception details:", e); + return null; + } catch (org.springframework.web.client.ResourceAccessException e) { + log.error("=== NETWORK/CONNECTION ERROR accessing OpenWeatherMap API ==="); + log.error("Error message: {}", e.getMessage()); + log.error("This could indicate: DNS resolution failure, network connectivity issues, firewall blocking, or SSL certificate problems"); + log.error("API URL attempted: {}", OPENWEATHERMAP_API_URL); + log.error("Exception details:", e); return null; } catch (org.springframework.web.client.RestClientException e) { - log.error("REST CLIENT EXCEPTION calling OpenWeatherMap API: {}", e.getMessage(), e); + log.error("=== REST CLIENT EXCEPTION calling OpenWeatherMap API ==="); + log.error("Exception type: {}", e.getClass().getName()); + log.error("Error message: {}", e.getMessage()); + log.error("Exception details:", e); return null; } catch (Exception e) { - log.error("UNEXPECTED EXCEPTION fetching current weather: {} - {}", - e.getClass().getSimpleName(), e.getMessage(), e); + log.error("=== UNEXPECTED EXCEPTION fetching current weather ==="); + log.error("Exception type: {}", e.getClass().getName()); + log.error("Error message: {}", e.getMessage()); + log.error("Activity ID: {}", activityId); + log.error("Coordinates: lat={}, lon={}", lat, lon); + log.error("Full stack trace:", e); return null; } } @@ -201,8 +248,10 @@ public class WeatherService { * Parse OpenWeatherMap API response and create WeatherData entity. */ private WeatherData parseWeatherResponse(String response, UUID activityId) { + log.debug("=== parseWeatherResponse START === activityId={}", activityId); try { JsonNode root = objectMapper.readTree(response); + log.debug("JSON parsed successfully, root node present: {}", root != null); WeatherData weatherData = new WeatherData(); weatherData.setActivityId(activityId); @@ -210,35 +259,54 @@ public class WeatherService { // Main temperature data if (root.has("main")) { JsonNode main = root.get("main"); + log.debug("Parsing 'main' section: {}", main); weatherData.setTemperatureCelsius(getBigDecimal(main, "temp")); weatherData.setFeelsLikeCelsius(getBigDecimal(main, "feels_like")); weatherData.setHumidity(getInteger(main, "humidity")); weatherData.setPressure(getInteger(main, "pressure")); + log.debug("Extracted main data: temp={}, feels_like={}, humidity={}, pressure={}", + weatherData.getTemperatureCelsius(), weatherData.getFeelsLikeCelsius(), + weatherData.getHumidity(), weatherData.getPressure()); + } else { + log.warn("Response JSON does not contain 'main' section"); } // Wind data if (root.has("wind")) { JsonNode wind = root.get("wind"); + log.debug("Parsing 'wind' section: {}", wind); weatherData.setWindSpeedMps(getBigDecimal(wind, "speed")); weatherData.setWindDirection(getInteger(wind, "deg")); + log.debug("Extracted wind data: speed={} m/s, direction={} degrees", + weatherData.getWindSpeedMps(), weatherData.getWindDirection()); + } else { + log.debug("Response JSON does not contain 'wind' section"); } // Weather condition if (root.has("weather") && root.get("weather").isArray() && !root.get("weather").isEmpty()) { JsonNode weather = root.get("weather").get(0); + log.debug("Parsing 'weather' array (first element): {}", weather); weatherData.setWeatherCondition(getString(weather, "main")); weatherData.setWeatherDescription(getString(weather, "description")); weatherData.setWeatherIcon(getString(weather, "icon")); + log.debug("Extracted weather condition: main='{}', description='{}', icon='{}'", + weatherData.getWeatherCondition(), weatherData.getWeatherDescription(), + weatherData.getWeatherIcon()); + } else { + log.warn("Response JSON does not contain valid 'weather' array"); } // Clouds if (root.has("clouds")) { weatherData.setCloudiness(getInteger(root.get("clouds"), "all")); + log.debug("Extracted cloudiness: {}%", weatherData.getCloudiness()); } // Visibility if (root.has("visibility")) { weatherData.setVisibilityMeters(root.get("visibility").asInt()); + log.debug("Extracted visibility: {} meters", weatherData.getVisibilityMeters()); } // Rain @@ -246,6 +314,7 @@ public class WeatherService { JsonNode rain = root.get("rain"); if (rain.has("1h")) { weatherData.setPrecipitationMm(BigDecimal.valueOf(rain.get("1h").asDouble())); + log.debug("Extracted rain: {} mm/h", weatherData.getPrecipitationMm()); } } @@ -254,6 +323,7 @@ public class WeatherService { JsonNode snow = root.get("snow"); if (snow.has("1h")) { weatherData.setSnowMm(BigDecimal.valueOf(snow.get("1h").asDouble())); + log.debug("Extracted snow: {} mm/h", weatherData.getSnowMm()); } } @@ -263,20 +333,34 @@ public class WeatherService { if (sys.has("sunrise")) { weatherData.setSunrise(LocalDateTime.ofInstant( Instant.ofEpochSecond(sys.get("sunrise").asLong()), ZoneId.systemDefault())); + log.debug("Extracted sunrise: {}", weatherData.getSunrise()); } if (sys.has("sunset")) { weatherData.setSunset(LocalDateTime.ofInstant( Instant.ofEpochSecond(sys.get("sunset").asLong()), ZoneId.systemDefault())); + log.debug("Extracted sunset: {}", weatherData.getSunset()); } } weatherData.setFetchedAt(LocalDateTime.now()); weatherData.setDataSource("openweathermap"); + log.info("Successfully parsed complete weather data"); + log.debug("=== parseWeatherResponse END === success=true"); return weatherData; + } catch (com.fasterxml.jackson.core.JsonProcessingException e) { + log.error("=== JSON PARSING ERROR ==="); + log.error("Failed to parse weather response as JSON"); + log.error("Response content: {}", response); + log.error("Parse error: {}", e.getMessage(), e); + return null; } catch (Exception e) { - log.error("Error parsing weather response: {}", e.getMessage()); + log.error("=== UNEXPECTED ERROR parsing weather response ==="); + log.error("Exception type: {}", e.getClass().getName()); + log.error("Error message: {}", e.getMessage()); + log.error("Response content: {}", response); + log.error("Full stack trace:", e); return null; } } @@ -303,23 +387,41 @@ public class WeatherService { // Helper methods to safely extract values from JSON private BigDecimal getBigDecimal(JsonNode node, String field) { - if (node.has(field)) { - return BigDecimal.valueOf(node.get(field).asDouble()); + if (node != null && node.has(field) && !node.get(field).isNull()) { + try { + return BigDecimal.valueOf(node.get(field).asDouble()); + } catch (Exception e) { + log.warn("Failed to extract BigDecimal from field '{}': {}", field, e.getMessage()); + return null; + } } + log.debug("Field '{}' not found or is null in node", field); return null; } private Integer getInteger(JsonNode node, String field) { - if (node.has(field)) { - return node.get(field).asInt(); + if (node != null && node.has(field) && !node.get(field).isNull()) { + try { + return node.get(field).asInt(); + } catch (Exception e) { + log.warn("Failed to extract Integer from field '{}': {}", field, e.getMessage()); + return null; + } } + log.debug("Field '{}' not found or is null in node", field); return null; } private String getString(JsonNode node, String field) { - if (node.has(field)) { - return node.get(field).asText(); + if (node != null && node.has(field) && !node.get(field).isNull()) { + try { + return node.get(field).asText(); + } catch (Exception e) { + log.warn("Failed to extract String from field '{}': {}", field, e.getMessage()); + return null; + } } + log.debug("Field '{}' not found or is null in node", field); return null; } }